10 Commits

Author SHA1 Message Date
Igor20264 e6b58efeb3 v0.5.3
Example document / build (push) Has been cancelled
Python application / build (push) Has been cancelled
- Мелкие исправления увеличивающие работу конвертера
2026-09-10 10:50:15 +03:00
Igor20264 510f7e7adf v0.5.2
Python application / build (push) Has been cancelled
Что то сделал
2026-09-08 19:37:54 +03:00
Igor20264 1a5b35eb54 BigUpdate 0.5.0
Python application / build (push) Has been cancelled
- add Local Render Mermaid
- add page-starе для указания смещения страниц
- add Гиперссылки в документе на списки литературы
2026-09-07 09:54:33 +03:00
Igor20264 ac0aa33399 hot-fix Build.bat 0.4.5
Python application / build (push) Has been cancelled
2026-09-06 11:13:41 +03:00
Igor20264 818a044aa1 update 0.4.4
Python application / build (push) Has been cancelled
- update документация
- промт для ии полу конфигурируемый
2026-09-06 11:04:01 +03:00
hleb 638fd38d7f fix-size src
Python application / build (push) Has been cancelled
- Удалена папка предназначенная для сборки не влияющая на сборку и разработку
2026-09-05 12:46:26 +00:00
Igor20264 febf62c257 hot-fix 0.4.3
Python application / build (push) Has been cancelled
- fix Не появляется пустая странициа перед названием таблицы
- fix Не появляется пустая страница перед альбомной ориентацией
- fix При отказе от изменений создаётся новый файл
- fix Замена "..." на «...» больше не затрагивает uml и mermaid блоки
2026-09-05 10:24:13 +03:00
Igor20264 8508d04bbc Small fix 0.4.2
Python application / build (push) Has been cancelled
- Update gitignore
- Rewrite blocks text
- Add custom "Отступы от края страницы"
2026-09-05 09:47:57 +03:00
Igor20264 2ba6fe2673 0.4.1
Python application / build (push) Has been cancelled
- fix Не собиралось само список всех приложений
2026-09-04 22:48:33 +03:00
Igor20264 b38661f588 Update 0.4.0
Python application / build (push) Has been cancelled
- Add\Rework UI
- Add Split Table and Listing
- Add Support Customazeble schems
2026-09-04 22:28:39 +03:00
179 changed files with 31809 additions and 9203 deletions
+2
View File
@@ -0,0 +1,2 @@
*.bat text eol=crlf
*.cmd text eol=crlf
+10 -9
View File
@@ -1,13 +1,13 @@
# This workflow will install Python dependencies, run tests and lint with a single version of Python # Builds examples/example.md into a DOCX and uploads it as a build artifact.
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
name: Python application name: Example document
on: on:
push: push:
branches: [ "main" ] branches: [ "master" ]
pull_request: pull_request:
branches: [ "main" ] branches: [ "master" ]
permissions: permissions:
contents: read contents: read
@@ -18,15 +18,16 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Set up Python 3.11 - name: Set up Python 3.11
uses: actions/setup-python@v3 uses: actions/setup-python@v5
with: with:
python-version: "3.11" python-version: "3.11"
- name: Install dependencies - name: Install dependencies
run: | run: |
echo ttf-mscorefonts-installer msttcorefonts/accepted-mscorefonts-eula select true | sudo debconf-set-selections echo ttf-mscorefonts-installer msttcorefonts/accepted-mscorefonts-eula select true | sudo debconf-set-selections
sudo apt-get install ttf-mscorefonts-installer sudo apt-get update
sudo apt-get install -y ttf-mscorefonts-installer
python -m pip install --upgrade pip python -m pip install --upgrade pip
python -m venv venv python -m venv venv
source venv/bin/activate source venv/bin/activate
@@ -35,7 +36,7 @@ jobs:
run: | run: |
source venv/bin/activate source venv/bin/activate
python -m md2gost --syntax-highlighting examples/example.md python -m md2gost --syntax-highlighting examples/example.md
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v4
with: with:
name: example name: example
path: example.docx path: examples/example.docx
+40
View File
@@ -31,6 +31,8 @@ MANIFEST
# before PyInstaller builds the exe, so as to inject date/other infos into it. # before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest *.manifest
*.spec *.spec
!md2gost.spec
!md2gost.fast.spec
# Installer logs # Installer logs
pip-log.txt pip-log.txt
@@ -130,6 +132,12 @@ venv.bak/
# md2gost diagram PNG cache # md2gost diagram PNG cache
.md2gost-cache/ .md2gost-cache/
md2gost/vendor/*.jar
md2gost/vendor/graphviz/
# Local ArchiMate / EA reference PDFs (licensed; not for the repo)
*.pdf
!docs/**/*.pdf
# LaTeX build artefacts # LaTeX build artefacts
*_latex/ *_latex/
@@ -172,3 +180,35 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear # and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/ .idea/
# md2gost runtime (created next to app / cwd on first run)
md2gost.schemes.json
md2gost.include-cache.json
# Student profile: real ФИО/группа, must stay local (see md2gost.user.json.example)
md2gost.user.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
# Dependency management is uv + uv.lock only
/poetry.lock
# Conversion output and scratch files kept out of the repo
/tmp_*.md
/tmp_*.docx
/tmp_*.png
/tmp_*.svg
/tmp_*.puml
/ReadMe.docx
md2gost/README.docx
/README.md1
/ad/
examples/example.docx
examples/example_out.docx
examples/toc_*.docx
tests/_idef0_preview*
latex/mirea/Settings/*.otf
+1
View File
@@ -0,0 +1 @@
3.12
+746
View File
@@ -0,0 +1,746 @@
# GhostEditor / md2gost — полное описание проекта
Версия пакета: **0.1.0**. Python **≥ 3.10**.
Репозиторий: конвертер учебных отчётов **Markdown → DOCX** по методичке РТУ МИРЭА (ГОСТ 7.32-2017) и профилям **ПИС / АПИД**. Рядом — два дополнительных пайплайна: **FODT** (LibreOffice) и **LaTeX → PDF**.
Этот файл — карта всего репозитория: что уже работает, как устроен код, какие функции есть в CLI/GUI и какие модули за что отвечают. Пользовательская документация по разделам лежит в [`docs/`](docs/).
---
## 1. Зачем проект существует
Студент пишет отчёт в расширенном Markdown (диалект md2gost). Конвертер сам:
- оформляет DOCX стилями Times New Roman, полями 30/10/20/20 мм, интервалом 1.5;
- нумерует рисунки, таблицы, листинги, формулы;
- собирает содержание и список источников;
- рисует UML / C4 / Mermaid;
- проверяет структуру по ТЗ (`--check`);
- при необходимости режет длинные таблицы и листинги через Microsoft Word и пишет «Продолжение…».
Исходный форк: [benzlokzik/md2gost](https://github.com/benzlokzik/md2gost). Здесь — своя линия: профили МИРЭА/ПИС/АПИД, GUI, схемы, стили JSON, Word COM, FODT, LaTeX.
---
## 2. Три продукта в одном репозитории
| Пакет | Entry point | Результат | Когда |
|--------|-------------|-----------|--------|
| **md2gost** | `python -m md2gost` / `md2gost-gui` | `.docx` | основной путь, Word |
| **md2fodt** | `python -m md2fodt` | `.fodt` | LibreOffice, без Word |
| **md2latex** | `python -m md2latex` | каталог XeLaTeX (+ `--pdf`) | PDF через шаблон МИРЭА |
| **word2md** | `python -m word2md` / `md2gost файл.docx` | `.md` | импорт DOCX → диалект md2gost |
Общий диалект разметки: спецразделы `# *ВВЕДЕНИЕ`, метки `%id`, ссылки `@Рисунок:id`, таблицы с `^`/`>`, формулы `$$`, библиография `[n]: …`.
Полнота реализации **не одинаковая**:
- **md2gost** — полный конвейер (стили, профили, диаграммы, checker, GUI, Word COM).
- **md2fodt** — v1: текст, заголовки, списки, таблицы с merge, картинки, листинги, простые ссылки, библиография. Нет PlantUML, нет «Продолжение таблицы», нет полного паритета PIS/АПИД.
- **md2latex** — тот же диалект → `content.tex` + копия `latex/mirea/`. Таблицы идут в `longtable` с настоящим «Продолжение таблицы N» (это умеет LaTeX, не Word).
- **word2md** — эвристический DOCX → диалект md2gost (стили Caption/Heading, подписи, таблицы, Code, формулы OMML). Не восстанавливает UML/Mermaid и исходные `%id`.
---
## 3. Как запустить
```bash
pip install -e .
python -m md2gost # GUI
python -m md2gost report.md -o report.docx --type coursework --check
python -m word2md report.docx -o report.md
python -m md2fodt report.md -o report.fodt
python -m md2latex report.md -o report_latex --pdf
```
Windows exe: `build-exe.bat` → `dist\md2gost.exe` (двойной клик — GUI; из консоли работает как CLI; перед сборкой тянет plantuml.jar и portable Graphviz).
Быстрая сборка: `build-exe-fast.bat` + `md2gost.fast.spec`.
Для режима продолжения таблиц/листингов `word` (по умолчанию): Windows + Microsoft Word + `pip install pywin32` (опциональная группа `[word]`).
---
## 4. Архитектура md2gost (главный конвейер)
Один движок на CLI и GUI. GUI только собирает `ConvertRequest` и зовёт `convert()`.
```
.md
│
├─ checker.check_markdown() # если --check / --check-only
│
├─ profiles.preprocess_markdown() # «ёлочки», тире; не трогает UML/Mermaid
│
├─ Parser (marko + extended_markdown)
│ └─ RenderableFactory → список Renderable
│
├─ biblio_processor.fold_bibliography()
├─ Heading / ToC / Table / Listing: режимы нумерации и continuation
├─ label_pass.assign_numbers() + resolve @Тип:id
│
├─ Renderer.process() # LayoutTracker, секции, альбом, колонтитулы
├─ TocProcessor.process() # native TOC field или ручная сборка
│
├─ [опционально] docxcompose: титул + задание + тело
├─ document.save()
├─ [если word] word_fix.fix_continuations() # COM: реальные разрывы страниц
└─ [если --check-pages] page_fill_check # эвристика полупустых страниц
```
Ключевые типы:
- `ConvertRequest` / `ConvertResult` — контракт CLI↔GUI (`md2gost/pipeline.py`).
- `DocProfile` — правила типа работы (`md2gost/profiles.py`).
- `StyleConfig` — поля страницы + стили абзацев (`md2gost/style_config.py`).
- `Renderable` — элемент, который умеет себя нарисовать в DOCX с учётом высоты страницы.
---
## 5. Что уже умеет пользователь (функции продукта)
### 5.1. Типы документов (`--type`)
| Тип | Стили | Нумерация объектов | Источники | Структура |
|-----|--------|-------------------|-----------|-----------|
| `practice` | mirea | по разделам `1.1`, `2.1` | 5–7, ≤5 лет | Введение, разделы, заключение, список, приложения |
| `coursework` | mirea | то же | 5–7, ≤5 лет | то же |
| `vkr` | mirea | то же | ≥10 **в каждом** разделе списка, ссылки `[1.5]` | + приложение «Графический материал» |
| `PIS_custom` | pis_custom | сквозная `1, 2, 3` | не обязательны | H1 = «Практическая работа №N. …»; H1 по центру ПРОПИСНЫМИ; H2 с абзацным отступом |
| `APID_coursework` | mirea | по разделам | 7–20, ≤5 лет | обязательные главы «Теоретические…» / «Прикладные…» и пункты 2.1–2.4 |
Реализация: `PROFILES` и `get_profile()` в `profiles.py`. Стили пресетов: `preset_mirea()`, `preset_pis_custom()`.
### 5.2. CLI (все флаги)
Точка входа: `md2gost/__main__.py` → `build_parser()`, `request_from_args()`, `main()`.
| Флаг | По умолчанию | Что делает |
|------|----------------|------------|
| `filename` | нет → GUI | `.md` или `.docx` с `--check-pages` |
| `--gui` | | открыть окно (можно сразу передать `.md`) |
| `-o` / `--output` | рядом с md, то же имя | выходной DOCX |
| `-t` / `--template` | `md2gost/Template.docx` | шаблон Word |
| `--type` | `practice` | профиль документа |
| `--heading-numbering` | `manual` | `manual` — цифры из md, автонумерация Word выкл.; `auto` — нумерует Word, цифры в начале заголовка снимаются |
| `--toc` | `native` | `native` — поле Word `TOC`; `manual` — сборка по LayoutTracker |
| `--table-continuation` | `word` | `word` / `off` / `soft` / `legacy` / `caption` |
| `--listing-continuation` | `word` | то же для листингов |
| `--table-repeat-header` | выкл. | шапка на фрагментах после word-split |
| `--emdash-to-hyphen` | выкл. | «—» → «-» |
| `--hr-pagebreak` | выкл. | `---` / `***` / `___` → разрыв страницы |
| `--title` / `--assignment` | | DOCX титула и бланка задания (склейка через docxcompose) |
| `--auto-title` / `--no-auto-title` | | автотитул из yaml/` ```title ` (по умолчанию выкл.) |
| `--student` / `--group` | | ФИО и группа для автотитула (иначе `md2gost.user.json`) |
| `--doc-author-from` / `--doc-author` | `os` / профиль | автор в свойствах DOCX: `os` (имя ОС), `student`, `custom` |
| `--doc-title` / `--doc-subject` / `--doc-keywords` / `--doc-comments` / `--doc-category` / `--doc-last-modified-by` | см. профиль | остальные поля File → сведения; пустые comments — без штампа md2gost |
| `--check` / `--check-only` / `--strict` | | проверка ТЗ; `--strict` → код выхода 1 при ошибках |
| `--check-pages` | | полупустые страницы через Word COM (эвристика, не влияет на `--strict`) |
| `--syntax-highlighting` | | Pygments в листингах |
| `--plantuml-jar` / `--kroki-url` | | локальный рендер диаграмм |
| `--diagram-fallback` | `remote` | `remote` / `local` / `off` |
| `--diagram-format` | `png` | `png` или `svg` (svgBlip + PNG-запасной) |
| `--diagram-scale` | `2.0` | качество PNG PlantUML (размер на странице как при 1) |
| `--schemes` | авто | путь к `md2gost.schemes.json` |
| `--styles` | авто | оверлей `md2gost.styles.json` |
| `--debug` | | отладочная сетка в документе + открыть файл |
Занятый выходной файл: `timestamped_output_path()` пишет `имя_ГГГГ-ММ-ДД-ЧЧ-ММ.docx`.
Windowed exe: `_argv_needs_console()` + `_enable_windows_console()` — консоль появляется только для CLI/`--help`, не для GUI.
### 5.3. GUI (Tkinter)
Модуль `md2gost/gui.py`, класс `Md2GostApp`. Entry: `md2gost-gui` / `python -m md2gost` без файла / `--gui`.
Уже есть:
- зона Drop: перетаскивание `.md` (tkinterdnd2, иначе Win32 `WM_DROPFILES`, иначе клик → диалог);
- основные параметры: тип, нумерация заголовков, TOC, continuation таблиц/листингов, тире, `---` → разрыв, проверки ТЗ, вёрстка, открыть после сборки;
- **Настройки → Файлы**: шаблон, титул, задание, стили JSON, выходной путь;
- **Настройки → Студент…**: ФИО и группа (первый запуск);
- **Настройки → Метаданные…**: свойства DOCX (автор, название, тема, теги, примечание) в `md2gost.user.json`;
- **Настройки → Диаграммы**: jar, Kroki, fallback, формат, масштаб, статус движка, скачать PlantUML, сброс кэша includes;
- **Шаблоны UML**: CRUD схем (`md2gost.schemes.json`) — добавить / дублировать / удалить / сохранить / открыть JSON;
- **Справка**: Инструкция, Документация (вшитые `docs/*.md`), Схемы, Промпт для ИИ (каталог `prompts/` + кнопки схем);
- конвертация в фоне (`threading`), лог в окне, тултипы, меню «Дебаг» на одну следующую сборку;
- если выходной DOCX занят — timestamp-имя, как в CLI.
Конвертация не блокирует окно: `_convert()` → поток → `_on_done()`.
### 5.4. Диалект Markdown
Реализовано в `extended_markdown/` (расширение Marko + GFM) и `RenderableFactory`.
| Элемент | Синтаксис | Поведение |
|---------|-----------|-----------|
| Спецраздел без номера | `# *ВВЕДЕНИЕ` | ПРОПИСНЫЕ, без автономера раздела |
| Содержание | `# *СОДЕРЖАНИЕ` + `[TOC]` | native поле Word или ручная сборка |
| Рисунок-файл | `![…](file.png "%id Подпись")` | нумерация + `@Рисунок:id` |
| Таблица | `%id Подпись` перед таблицей | подпись «Таблица N — …» |
| Merge ячеек | `^` rowspan, `>` colspan | OOXML `vMerge` / `gridSpan`; через разрыв страницы merge не переносится |
| Листинг | `%id` + code fence | Courier New, опционально Pygments |
| Диаграмма | `%id` + ` ```uml ` / `uml-c4` / `mermaid` / `idef0` / `dfd` | Рисунок; флаги `+listing`, `+landscape` |
| Формула | `%eq1` + `$$…$$` + `@Формула:eq1` | номер только если есть ссылка |
| Инлайн-формула | `$…$` | OMML в абзаце |
| Ссылка на объект | `@Рисунок:id`, `@Таблица:id`, `@Листинг:id`, `@Формула:id` | подстановка номера |
| Источник | `[1]` в тексте, `[1]: …` в списке | ВКР: `[1.5]`, цитата `[2.18, c. 21-25]` |
| Разрыв страницы | `---` + `--hr-pagebreak` | иначе HR игнорируется |
| Жирный / курсив / зачёркивание / ссылки / списки | обычный MD | поддерживается |
Автоправки текста (`preprocess_markdown`):
- `"..."` → «…» (эвристика, не внутри `%…`);
- `слово - слово` → `слово — слово` (если не `--emdash-to-hyphen`);
- внутри оград UML/Mermaid/схем **не** трогает кавычки и тире;
- в списке источников каждый `[n]:` выносится в отдельный абзац (`separate_biblio_lines`).
### 5.5. Схемы и диаграммы
Встроенные id (`md2gost/diagrams/schemes.json`): `c4`, `c4context`, `c4component`, `usecase`.
Слои загрузки (позже побеждает): встроенный шаблон → `md2gost.schemes.json` рядом с приложением (создаётся при первом запуске, не перезаписывается) → рядом с `.md` → `--schemes`.
Рендер UML:
1. Java + `plantuml.jar` (вшитый / `%LOCALAPPDATA%\md2gost\` / `--plantuml-jar`);
2. локальный Kroki (`KROKI_URL` / `--kroki-url`);
3. `https://kroki.io` при `--diagram-fallback remote`.
Mermaid — локально (браузер / QuickJS) или Kroki. **IDEF0** — оградка ` ```idef0 `, локальный Pillow (рамка NIST). **DFD** — оградка ` ```dfd `, пакет [data-flow-diagram](https://github.com/pbauermeister/dfd) → вшитый/кэш/PATH Graphviz (`scripts/fetch_graphviz.py`, GUI «Скачать Graphviz») или Kroki.
Кэш картинок: `{каталог_md}/.md2gost-cache/`.
Кэш `!include` URL: `md2gost.include-cache.json` + `include-cache/`.
Альбомная страница: флаг `+landscape` у подписи — отдельная секция A4 landscape с вертикальным центрированием, затем снова книжная. Один section break (без пустой книжной страницы перед альбомом).
### 5.6. Кастомные стили JSON
Файл `md2gost.styles.json` (рядом с `.md` и/или `--styles`). Deep-merge поверх пресета типа. Неизвестный ключ или имя стиля — ошибка (`StyleConfigError`).
Можно менять поля `page` (мм) и поля стилей абзацев: шрифт, кегль, жирный/курсив/прописные, выравнивание, отступы, интервал 1.0/1.5, `page_break_before`, `keep_with_next`, `widow_control`.
Не в JSON: табы оглавления (считаются от ширины полосы), отступы списков, цвет (всегда чёрный), формулы из шаблона.
### 5.7. Проверка ТЗ (`--check`)
Модуль `md2gost/checker/__init__.py`. Возвращает `Issue(id, severity, message, line)`.
Уже проверяется:
- обязательные разделы по профилю (СОДЕРЖАНИЕ, ВВЕДЕНИЕ, ЗАКЛЮЧЕНИЕ, список; для ПИС — практические работы; для ВКР — «Графический материал»; для АПИД — 6 обязательных заголовков);
- `[TOC]`;
- спецразделы должны быть `# *…`, лучше ПРОПИСНЫМИ;
- запрет «рис.», «табл.», сносок `[^1]`, графы «№ п/п»;
- ссылки `[n]` во введении/заключении запрещены;
- число и возраст источников; для ВКР — по разделам списка; порядок первого упоминания;
- висячие / неиспользованные метки `%id` / `@Тип:id`;
- запрещённые буквы приложений (Ё З Й О Ч Ь Ы Ъ);
- перечень приложений после `# *ПРИЛОЖЕНИЯ`, если приложений ≥ 2;
- маркеры merge `^`/`>` в шапке и `>` в первом столбце;
- подсказка про «Продолжение…», если continuation = `off`/`soft`.
`--strict` падает только на `severity == "error"`. `--check-pages` — отдельная эвристика, в strict не входит.
### 5.8. Продолжение таблиц и листингов
DOCX сам не умеет писать «Продолжение Таблицы N» только со 2-й страницы.
| Режим | Поведение |
|--------|-----------|
| **`word`** (по умолчанию) | После save Word COM режет по реальной пагинации, вставляет подпись |
| **`off` / `soft`** | Одна таблица/листинг, пагинацию делает Word, без автоподписи |
| **`legacy`** | Резка по оценке высоты LayoutTracker + `page_break_before` (могут быть дыры) |
| **`caption`** | То же + явный PageBreak |
Оценка высоты ≠ вёрстка Word — поэтому `legacy`/`caption` помечены как неточные.
### 5.9. Титул, задание, колонтитулы
`--title` / `--assignment`: пустая оболочка из шаблона + docxcompose (титул, разрыв, задание, разрыв, тело).
На секциях титула/задания колонтитул без номера страницы; дальше — сквозная нумерация, поле `PAGE`, TNR 12 по центру. После TOC начинается секция тела с номерами.
Если в документе нет TOC — номер ставится на единственную секцию.
**Автотитул** (по умолчанию **выкл.**; CLI `--auto-title` / галочка в GUI; явный `--title` отключает генератор): см. [`docs/title.md`](docs/title.md). Кратко: `md2gost/title_page.py` заполняет bundled `TitleTemplate.docx` из `info_conv.yaml` и/или блока ` ```title `, ФИО/группа — `md2gost.user.json` или `--student`/`--group`. GUI: первый запуск и **Настройки → Студент…**.
**Свойства DOCX** (после склейки титула, перед save): `doc_metadata.apply_document_metadata`. По умолчанию автор = `getuser()`, примечание «Создано при помощи md2gost (ТЗ МИРЭА)», created/modified = сейчас, revision = 1. GUI **Настройки → Метаданные…** / флаги `--doc-*`. Профиль — блок `metadata` в `md2gost.user.json`.
### 5.10. Приложения
- Буквы: А Б В Г Д Е Ж И К … (без Ё З Й О Ч Ь Ы Ъ) — `APPENDIX_LETTERS` в `numberer.py`.
- `## Приложение А Название` оформляется как приложение.
- Если после `# *ПРИЛОЖЕНИЯ` нет перечня, а приложений несколько — Renderer **сам вставляет** список «Приложение А — …». Если автор уже написал перечень — не дублирует.
### 5.11. Промпты для ИИ
| Файл | Назначение |
|------|------------|
| `prompts/generate-md.md` | диалект md2gost vs обычный MD |
| `prompts/generate-mirea-report.md` | полный отчёт ГОСТ / МИРЭА |
| `prompts/generate-pis-custom-report.md` | итоговый отчёт ПИС |
| `prompts/emulate-student.md` | голос: человечный текст, не отчёт ИИ |
В GUI: выбор промпта + чекбоксы схем → в конец дописываются `ai-prompt` и макросы из `md2gost.schemes.json` (`compose_prompt()`).
### 5.12. Сборка и поставка
- `pyproject.toml`: hatchling, пакеты `md2gost`, `md2latex`, `md2fodt`; в wheel вшиваются Template.docx, mml2omml, diagrams, latex/mirea, prompts, docs.
- `md2gost.spec` / `md2gost.fast.spec` — PyInstaller (вшивают `plantuml.jar` и при наличии `vendor/graphviz`).
- `scripts/fetch_plantuml.py` — скачать jar.
- `scripts/fetch_graphviz.py` — скачать portable Graphviz в `md2gost/vendor/graphviz` (для DFD в exe).
- `scripts/md2gost_exe.py` — обёртка exe.
- `scripts/pyi_rth_mplbackend.py` — runtime hook matplotlib для PyInstaller.
- `scripts/check_page_fill.bas` — макрос Word для той же эвристики вёрстки.
- `scripts/md2pdf.ps1` / `md2pdf.sh` — обёртки LaTeX→PDF.
- CI: `.github/workflows/pytest.yml` (Python 3.11, flake8, pytest); `example-generator.yml`.
---
## 6. Карта модулей md2gost и функции кода
Ниже — публичные и важные внутренние функции **уже реализованные**. Приватные `_foo` указаны, если без них не понять модуль.
### 6.1. Точки входа и пайплайн
**`md2gost/__init__.py`**
- `package_dir()` — каталог пакета (и `_MEIPASS` в frozen exe).
**`md2gost/__main__.py`**
- `build_parser()` — argparse.
- `request_from_args(args)` → `ConvertRequest`.
- `_argv_needs_console(argv)`, `_enable_windows_console()`.
- `main()`.
**`md2gost/pipeline.py`**
- `ConvertRequest`, `ConvertResult`.
- `default_output_path(filename)`, `timestamped_output_path(path)`, `default_template_path()`.
- `open_document(path)` — macOS `open` / Windows `startfile` / `xdg-open`.
- `_fix_front_matter_after_compose(...)` — сброс PAGE на титуле/задании, сквозная нумерация.
- `convert(req, log=None)` — весь конвейер, ошибки в `ConvertResult`, не пробрасывает исключение наружу. Перед save — `apply_document_metadata`.
- `should_launch_gui(filename, gui_flag)`.
- `_CallbackLogHandler` — логи md2gost в GUI/CLI.
**`md2gost/doc_metadata.py`**
- `DocumentMetadata`, `DEFAULT_DOC_COMMENTS`, `AUTHOR_SOURCES`.
- `os_user()`, `clip_core()` (лимит 255 символов python-docx).
- `resolve_document_metadata(...)` — профиль + CLI/request.
- `apply_document_metadata(document, meta)` — core properties + даты + revision=1.
**`md2gost/user_profile.py`**
- `UserProfile` (student, group, metadata), `load_user_profile` / `save_user_profile` → `md2gost.user.json`.
**`md2gost/converter.py`**
- класс `Converter`: читает md, применяет стили, парсит, нумерует, рендерит.
- `convert()`, свойства `document`, `style_config`, `raw_markdown`, `doc_type`, `heading_numbering`.
### 6.2. Профили и стили
**`md2gost/profiles.py`**
- константы: `DOC_TYPES`, `HEADING_NUMBERING_MODES`, `TOC_MODES`, `TABLE_CONTINUATION_MODES`, `LISTING_CONTINUATION_MODES`, `NUMBERING_SCOPES`, `STYLE_PRESETS`.
- `DocProfile` — поля min/max источников, sectional_biblio, require_graphic_appendix, style_preset, numbering_scope, флаги структуры, `require_apid_kr_structure`.
- `get_profile(doc_type)`.
- `fix_russian_quotes`, `fix_dashes`, `replace_emdash_with_hyphen`.
- `set_emdash_to_hyphen`, `emdash_to_hyphen_enabled`, `dash_separator`.
- `preprocess_markdown`, `separate_biblio_lines`.
- `find_formula_refs` — `@Формула:` / `@Formula:`.
- `current_year()`.
**`md2gost/style_config.py`**
- `StyleConfigError`.
- `PageSpec.merge/to_dict/require_complete`.
- `ParagraphStyleSpec.merge/to_dict`.
- `StyleConfig.merge/to_dict`.
- `preset_mirea()`, `preset_pis_custom()`, `get_preset(name)`.
- `style_config_from_dict`, `load_styles_file`, `resolve_style_config` (пресет ← рядом с md ← `--styles`).
**`md2gost/styles.py`**
- `apply_style_config(document, config)` — поля, секции, табы TOC, стили абзацев.
- `apply_mirea_styles`, `apply_pis_custom_styles`.
- `apply_document_styles(...)` — пресет + оверлей, возвращает итоговый `StyleConfig`.
- Внутри: `_set_run_font` (блокирует тему Word: Calibri/синий), `_ensure_style`, `_fix_toc_tab_stops`.
**`md2gost/page_geometry.py`**
- глобальные поля `MARGIN_*`, `MARGIN_PRESETS`, `apply_margin_preset`.
- `is_landscape_section`, `apply_section_geometry`, `content_size`.
- `apply_centered_page_footer`, `clear_section_footer`, `ensure_continuous_page_numbers`.
### 6.3. Разбор Markdown
**`md2gost/extended_markdown/`**
- `__init__.py` — Marko + GFM + Extension: Equation, Reference, Caption, Table, TOC, Heading, InlineEquation, Image.
- `heading.py` — `# *` → ненумерованный.
- `caption.py` — строка `%id текст +listing +landscape`.
- `table.py` — GFM-таблица + `^`/`>` + `_resolve_merge_restarts`.
- `image.py` — `%id` в title картинки.
- `equation.py` — блочная `$$`.
- `inline_formula.py` — `$…$`.
- `reference.py` — `@Тип:id`.
- `toc.py` — `[TOC]`.
**`md2gost/parser_.py`**
- `Parser.parse()` — пропускает пустые строки; копит `CaptionInfo`; HR без флага игнорирует; иначе `factory.create`.
**`md2gost/renderable_factory.py`**
- `RenderableFactory.create` (singledispatch): Paragraph, Heading, FencedCode/CodeBlock → Listing или DiagramFigure, Equation, List, Table, TOC, ThematicBreak → PageBreak.
- `_create_runs` — жирный/курсив/зачёркивание, код, картинки, инлайн-формулы, `@ссылки`, гиперссылки.
### 6.4. Нумерация и ссылки
**`md2gost/numberer.py`**
- `Numberer(mode="section"|"continuous")`: `enter_section`, `enter_appendix`, `next_number`, `resolve`, `register_label`.
- Режим section: `1.1` / `Б.1`, счётчики сбрасываются на разделе/приложении.
- Режим continuous: `1, 2, 3` на весь документ (ПИС).
**`md2gost/label_pass.py`**
- `LabelRegistry.put/get/get_display` (алиасы Рисунок/Figure/Таблица/…).
- `assign_numbers(renderables, numbered_equations, numbering_scope)`.
- `resolve_pending_in_renderables`, `set_active_registry`, `resolve_reference`.
Формулы нумеруются **только** если на них есть `@Формула:…` в исходнике.
### 6.5. Рендер в DOCX
**`md2gost/renderer.py` — класс `Renderer`**
- `process(renderables)` — основной цикл.
- Секции: фронт без номеров → после TOC тело с PAGE; `_enter_landscape` / `_exit_landscape`.
- `_ensure_appendix_index` — автоперечень приложений.
- Нумерация объектов через `Numberer` (если не `skip_numbering` — в Converter нумерация уже в label_pass).
- Спецзаголовки: СОДЕРЖАНИЕ / СПИСОК — по центру; Введение / Заключение / ПРИЛОЖЕНИЯ — слева как H1.
**`md2gost/layout_tracker.py`**
- `LayoutState` — текущая/оставшаяся высота, номер страницы.
- `LayoutTracker` — `add_height`, `can_fit_to_page`, `new_page`, `set_page_size` (книга ↔ альбом).
**`md2gost/renderable/`**
| Класс | Файл | Роль |
|--------|------|------|
| `Renderable` | `renderable.py` | абстрактный `render()` |
| `Paragraph` / `Link` | `paragraph.py` | абзац, runs, картинки, ссылки, инлайн-формулы |
| `Heading` | `heading.py` | уровни 1–9, manual/auto, снятие Word numbering, strip ведущих цифр |
| `Table` | `table.py` | сетка, merge, continuation, `+landscape` |
| `Listing` | `listing.py` | код, Pygments (`DocxParagraphPygmentsFormatter`), continuation |
| `Image` | `image.py` | файл-рисунок + подпись |
| `DiagramFigure` | `diagram.py` | рендер схемы + опциональный Listing исходника |
| `Equation` | `equation.py` | блочная формула OMML + номер справа |
| `Caption` / `CaptionInfo` | `caption.py` | «Рисунок 1.1 — …» |
| `List` | `list.py` | маркированные/нумерованные, вложенность |
| `ToC` | `toc.py` | native field `TOC \o "1-3" \h \z \u` или ручные строки с табами |
| `PageBreak` | `page_break.py` | явный разрыв |
| `RequiresNumbering` | `requires_numbering.py` | интерфейс нумеруемых объектов |
| `ParagraphSizer` / шрифты | `paragraph_sizer.py`, `find_font.py` | оценка высоты абзаца (FreeType) |
**`md2gost/docx_elements.py`**
- `create_table`, `create_table_row`, `create_table_cell`, `apply_cell_merge`, `set_table_box_borders`, фиксация ширины в twips.
**`md2gost/docx_svg.py`**
- `attach_svg_blip(run, inline_shape, svg_path)` — вектор + PNG fallback.
**`md2gost/latex_math.py`**
- `latex_to_omml(latex)` — latex2mathml + XSLT `mml2omml`.
- `inline_omml(omml)` — дроби в линейный вид для инлайна.
**`md2gost/toc_processor.py`**
- `TocProcessor.process` — native: `updateFields` при открытии Word; manual: страницы из `Heading.rendered_page` (СОДЕРЖАНИЕ в оглавление не попадает).
**`md2gost/debugger.py`**
- плавающая сетка страниц в документе (`--debug`): `Debugger`, `add_float_picture`, `new_pic_anchor`.
**`md2gost/util.py`**
- `create_element` — низкоуровневый OOXML.
### 6.6. Библиография
**`md2gost/bibliography.py`**
- `BiblioEntry`, `extract_year`, `parse_biblio_block`, `find_citations`.
- `Bibliography(...)` — ленивая обёртка над renderable (чтобы checker не тянул docx).
**`md2gost/biblio_processor.py`**
- `entries_from_text` — несколько `[n]:` внутри одного абзаца (Marko склеивает строки).
- `extract_bibliography_from_markdown` — плоский список или секции для ВКР.
- `fold_bibliography(renderables, parent, raw_markdown)` — заменяет абзацы списка на `Bibliography`.
**`md2gost/bibliography_renderable.py`** — отрисовка пунктов списка стилем Bibliography.
### 6.7. Диаграммы
**`md2gost/diagram_schemes.py`**
- `DiagramScheme` (id, title, docs, ai-prompt, includes, prefix/postfix, theme).
- `bundled_schemes_path`, `user_schemes_path`, `ensure_user_schemes`.
- `load_schemes_from_path`, `save_schemes`, `load_merged_schemes`.
- `scheme_id_from_lang`, `is_diagram_lang`, `get_scheme`, `get_schemes`, `reload_schemes`, `configure_schemes`.
- `apply_scheme`, `prepare_with_schemes`.
**`md2gost/diagram_includes.py`**
- кэш HTTP `!include`: `resolve_url`, `resolve_include_ref`, `rewrite_http_includes_in_source`, `reset_include_cache`, `cache_entry_count`.
**`md2gost/diagram_renderer.py`**
- `configure_diagrams(...)`, `DiagramConfig`, `DiagramRenderResult`.
- `prepare_source`, `render_diagram`.
- `resolve_plantuml_jar`, `fetch_plantuml_jar`, `iter_plantuml_candidates`, `diagram_engine_status`.
- рендер jar / Kroki / пара png+svg; масштаб `scale` в исходник PlantUML.
- локальные типы `LOCAL_ONLY_TYPES` (`idef0`, `dfd`): Pillow, без jar/Kroki.
**`md2gost/idef0.py`** — DSL IDEF0 + Pillow/SVG.
**`md2gost/dfd.py`** — обёртка [pbauermeister/dfd](https://github.com/pbauermeister/dfd): DSL → DOT → vendor/system Graphviz / Kroki (`write_dfd_outputs`, `fetch_graphviz`).
**`md2gost/diagrams/`** — `schemes.json`, C4 `*.puml`.
### 6.8. Word COM и вёрстка
**`md2gost/word_fix.py`**
- `parse_caption_text`, `find_page_break_row`, `continuation_label`, `caption_style_name`.
- `fix_continuations(path, tables, listings, repeat_header)` → `FixResult`.
- Внутри: до 5 проходов `Repaginate` + `Split`, рамки листингов, опциональный повтор шапки.
**`md2gost/page_fill_check.py`**
- `PageMetric`, `PageFillIssue`.
- `evaluate_page_fill(pages)` — чистая эвристика (порог пустого низа 40%, пропускает альбом, последнюю страницу, фронт, конец раздела, «в основном рисунок/таблица»).
- `check_docx_page_fill(path)`, `format_page_fill_report`.
### 6.9. Checker (функции проверок)
`md2gost/checker/__init__.py`:
- `check_structure`, `check_apid_kr_structure`, `check_pis_headings`, `check_special_headings`.
- `check_forbidden_abbreviations`, `check_citations_in_intro_conclusion`.
- `check_bibliography`, `check_object_refs`, `check_appendices`.
- `check_continuation_hints`, `check_table_merge`.
- `check_markdown(...)` — оркестратор.
- `format_report(issues)`.
### 6.10. GUI, DnD, справка
**`md2gost/gui.py`**
- `_make_root`, `_combo_values/_combo_key/_display`, `_Tooltip`, `_tip`.
- `Md2GostApp`: меню, окна файлов/студента/метаданных/диаграмм/схем/справки, браузер docs, сборка промпта, DnD, `_collect()` → `ConvertRequest`, `_convert`.
- `run_gui(initial)`, `main()`.
**`md2gost/dnd.py`**
- `parse_tkdnd_files`, `normalize_drop_paths`, `first_markdown`, `enable_file_drop`.
- Win32: `_WinDropHook`, `_Win32DropApi`, `_query_drop_files`.
**`md2gost/help_content.py`**
- константы `USAGE_HELP`, `SCHEMES_HELP`.
- `prompt_search_dirs`, `load_prompt_catalog`, `scheme_prompt_block`, `compose_prompt`.
- `docs_search_dirs`, `load_docs_catalog`.
---
## 7. md2fodt — что уже есть
Пакет `md2fodt/`.
| Файл | Функции |
|------|---------|
| `__main__.py` | `main()` — CLI: файл, `-o`, `--emdash-to-hyphen` |
| `converter.py` | `convert_md_to_fodt(md, output, emdash_to_hyphen)` |
| `emitter.py` | `emit_body`, `emit_fodt`; эмиттеры heading/paragraph/image/table/code/list/equation; merge `^`/`>` |
| `styles.py` | `styles_xml()`, `automatic_styles_xml()` — TNR 14, поля ГОСТ |
| `escape.py` | `escape_text`, `xml_id` |
Картинки встраиваются base64. Нет диаграмм PlantUML и нет «Продолжение таблицы».
---
## 8. md2latex — что уже есть
Пакет `md2latex/` + шаблон `latex/mirea/` (вендор [mirea-ninja/Latex-Template-for-Report-Diploma-Thesis](https://github.com/mirea-ninja/Latex-Template-for-Report-Diploma-Thesis)).
| Файл | Функции |
|------|---------|
| `__main__.py` | `main()`, `_run_xelatex` (latexmk или 2 прохода xelatex) |
| `converter.py` | `package_template_dir()`, `convert_md_to_latex_project(md, out, titlepage)` — копирует шаблон, пишет `content.tex` |
| `emitter.py` | `emit_document`; `_emit_longtable` с `\endfirsthead` / `\endhead` и «Продолжение таблицы N» |
| `escape.py` | `escape_text`, `escape_verbatim_for_listing`, `latex_label` |
Флаги: `-o` каталог, `--titlepage` PDF титула, `--pdf`, `--xelatex-passes`.
---
## 8.1. word2md — импорт DOCX
Пакет `word2md/`. Эвристика: стили md2gost, иначе ГОСТ-текст подписей/заголовков.
| Файл | Роль |
|------|------|
| `__main__.py` | CLI: `.docx`, `-o`, `--media-dir`, `--keep-toc-pages`, `--pagebreaks` |
| `pipeline.py` | `ImportRequest` / `ImportResult` / `convert_docx()` |
| `walker.py` | обход body, склейка Code/списков, `postprocess_blocks` (продолжения) |
| `classify.py` | Heading / Caption / Code / библио / спецразделы |
| `captions.py` | `Рисунок` / `Таблица` / `Листинг` + `parse_caption_text` из md2gost |
| `tables.py` | merge → `^`/`>`, детект формульной таблицы |
| `omml.py` | OMML → читаемая формула |
| `media.py` | выгрузка `word/media` |
| `emit.py` | IR → диалект md2gost |
| `refs.py` | `Рисунок 1.1` → `@Рисунок:fig1_1` |
Точки входа также: `python -m md2gost report.docx`, GUI «Импорт DOCX→MD».
---
## 9. Тесты — что покрыто
Каталог `tests/`, CI гоняет `pytest`.
| Файл | Что проверяет |
|------|----------------|
| `test_pipeline.py` | GUI/CLI развилка, пути, DnD, argparse, HR→pagebreak, промпты, docs catalog, listing continuation |
| `test_mirea_tz.py` | Numberer, checker (coursework/PIS/ВКР/АПИД), препроцесс, TOC, заголовки, приложения, библио, page-fill эвристика, стили H1 |
| `test_style_config.py` | merge оверлея, неизвестные ключи, CLI побеждает файл рядом с md, применение к Document |
| `test_table_merge.py` | `^`/`>`, OOXML merge, checker merge, режимы continuation |
| `test_word_fix.py` | разбор подписей, индекс разрыва, COM smoke (если нет Word — мягко) |
| `test_landscape.py` | геометрия альбома, секции, картинки/таблицы/листинги, один section break |
| `test_layout_tracker.py` | страницы и остаток высоты |
| `test_paragraph.py` / `test_paragraph_sizer.py` | абзацы и измерение шрифта |
| `test_diagram_renderer.py` | схемы, кэш, mermaid, svg, jar, `+listing`/`+landscape` |
| `test_idef0.py` | DSL IDEF0, PNG/SVG, интеграция `render_diagram` |
| `test_dfd.py` | DFD data-flow-diagram, mock Graphviz/Kroki, интеграция `render_diagram` |
| `test_md2fodt.py` | escape, каркас FODT, запись файла |
| `test_md2latex.py` | longtable continuation, генерация проекта |
| `test_word2md.py` | подписи, спецзаголовки, @ссылки, продолжения таблиц, DOCX→MD, round-trip |
| `extended_markdown/formula.py` | формулы (unittest) |
Нет полного e2e «открыть Word и сравнить каждую страницу» в Linux CI (шрифты ставятся, Word COM — нет).
---
## 10. Документация и примеры
Пользовательские страницы (те же вшиты в exe, GUI: Справка → Документация):
| Файл | Содержание |
|------|------------|
| `ReadMe.md` | краткий старт репозитория |
| `docs/README.md` | оглавление docs |
| `docs/quickstart.md` | установка, exe, типы |
| `docs/markdown.md` | диалект |
| `docs/schemes.md` | UML/Mermaid, JSON схем |
| `docs/types.md` | профили работ |
| `docs/cli-gui.md` | флаги ↔ GUI |
| `docs/styles.md` | JSON стилей |
| `docs/prompts.md` | ИИ-промпты |
| `docs/word2md.md` | импорт DOCX → Markdown |
| `md2gost/README.md` | справка пакета (подробнее про continuation и диаграммы) |
| `prompts/README.md` | как копировать промпты |
| `examples/example.md`, `examples/pis_custom.md` | образцы |
| `examples/md2gost.styles.json` | пример оверлея стилей |
**Источник требований к оформлению** — [`docs/reference/method-mirea-2022.txt`](docs/reference/method-mirea-2022.txt): извлечённый текст методических указаний РТУ МИРЭА (Кириллина Ю.В. и др., 2022, на базе ГОСТ 7.32-2017). Значения в `md2gost/style_config.py` и правила checker'а сверяются по нему — при спорных местах смотреть номера строк этого файла. В exe не вшивается (это не пользовательская страница). Извлечение местами побито (потеряны начала строк), сверять по исходному PDF.
---
## 11. Зависимости
Менеджер один: `[project]` в `pyproject.toml` + `uv.lock` (канонический lock-файл; `uv sync`). Poetry-метаданные, `poetry.lock` и `requirements.txt` удалены.
Обязательные (`pyproject.toml`): python-docx, marko, docxcompose, freetype-py, pillow, requests, latex2mathml, pygments, lxml.
Опционально: pywin32 (Word COM), matplotlib (сборка exe / отладка), tkinterdnd2 (удобный DnD в GUI).
Внешние рантаймы: Java + plantuml.jar **или** Kroki; Graphviz (вшитый/кэш/PATH) для DFD; для PDF — XeLaTeX / latexmk и шрифт Times New Roman.
---
## 12. Что в репозитории, но не продукт
Черновики и чужой код рядом с пакетами. Это **не** часть `pip install` / exe:
- корень: `todo.txt`, `delete_this.txt`;
- `ad/` — черновые отчёты (лежат локально, из git убраны);
- `_paco_extract/` — выгрузки методичек ПАЦО;
- `Other_Code/` — справка OOXML, docx-js, скрипты pack/unpack/validate (не импортируется md2gost);
- локальные артефакты: `tmp_*.{md,docx,png,svg,puml}`, `examples/*.docx`, `tests/_idef0_preview*`, `build-fast/`, кэши диаграмм — все в `.gitignore`;
- `md2gost.user.json` — профиль студента с реальными ФИО, в git не хранится (шаблон — `md2gost.user.json.example`).
Старые монолитные прототипы конвертера (`main.py`, `main12.py`, `mm2.py`…`mm5.py`, `asd.py`, корневые `parser.py` и `font.py`, `_audit_tmp.py`) удалены — их функциональность давно в `converter.py`, `renderer.py`, `style_config.py`. Не путать удалённый корневой `parser.py` с живым `md2gost/parser_.py` и корневой `font.py` с живым `md2gost/renderable/find_font.py`.
`todo.txt` сейчас: «оценки с текстом через сам Word, а не предугадывание» — частично закрыто режимом `word`.
---
## 13. Чего нет / ограничения (честно)
- DFD: пакет `data-flow-diagram` в зависимостях; картинка — вшитый/кэш/PATH Graphviz или Kroki; балансировка уровней не проверяется.
- FODT и LaTeX не догоняют md2gost по GUI, checker, схемам, профилям PIS/АПИД, Word COM.
- «Продолжение таблицы» в чистом DOCX без Word невозможно точно (нет longtable). Без Word — `off` или неточный `legacy`/`caption`.
- `--check-pages` даёт ложные срабатывания; это эвристика.
- Merge ячеек не живёт через границу разрезанной таблицы.
- Цвет текста в стилях не настраивается (всегда чёрный).
- Профиль `paco_custom` упомянут в комментарии `page_geometry`, отдельного типа в `DOC_TYPES` нет.
- Нумерация заголовков H4+ в стилях JSON не описана (в Word уровни 1–9 у Heading есть).
---
## 14. Типичные сценарии «уже работает»
1. **Практика / курсовая МИРЭА:** `# *ВВЕДЕНИЕ` … `# 1 …` … список `[1]:` → `--type practice|coursework --check --strict`.
2. **ВКР:** раздельный список, ссылки `[1.5]`, приложение «Графический материал».
3. **ПИС:** `# Практическая работа №1. …` → `--type PIS_custom`, сквозные Рисунок 1, 2, 3.
4. **АПИД:** обязательные 2 главы и 2.1–2.4, источники 7–20.
5. **UML в отчёте:** `%id Подпись` + ` ```uml-c4 ` — PNG/SVG в Word, опционально `+listing` и `+landscape`.
6. **Широкая таблица:** `%id … +landscape`.
7. **Длинная таблица на Windows:** режим `word` → COM режет и пишет «Продолжение Таблицы N».
8. **Свой вуз/кафедра:** `md2gost.styles.json` меняет поля и H1 без правки кода.
9. **Своя схема:** GUI «Шаблоны UML» или JSON с `includes`/`prefix`/`ai-prompt`.
10. **Черновик через ИИ:** промпт из GUI + схемы → `.md` → конвертер.
11. **Только проверить готовый DOCX:** `python -m md2gost report.docx --check-pages`.
12. **Без Word:** `python -m md2fodt` или `python -m md2latex --pdf`.
---
## 15. Поток данных одним абзацем
Пользователь кладёт `.md` (и опционально титул/задание/JSON стилей/схем). `convert()` читает текст, при необходимости гоняет checker, препроцессит кавычки/тире, парсит Marko-расширением в дерево, фабрика делает Renderable, библиография сворачивается в особые абзацы, объектам раздаются номера по профилю, `@ссылки` заменяются на «Рисунок 2.1», Renderer кладёт OOXML в очищенный Template.docx с оценкой высоты страницы, TocProcessor дописывает содержание, pipeline склеивает титул, сохраняет DOCX, при режиме `word` открывает файл в Word и режет таблицы/листинги по настоящей пагинации. GUI делает ровно то же, только параметры собирает из формы.
---
## 16. Где править, если добавлять функцию
| Хочешь | Куда |
|--------|------|
| Новый тип работы | `profiles.PROFILES` + checker + стили пресета |
| Новое поле CLI/GUI | `__main__.build_parser`, `ConvertRequest`, `gui._collect/_apply_request` |
| Новый элемент Markdown | `extended_markdown/` + `RenderableFactory` + класс в `renderable/` |
| Новая проверка ТЗ | `checker/__init__.py` → вызов из `check_markdown` |
| Новая UML-схема «из коробки» | `md2gost/diagrams/schemes.json` (+ `.puml` при необходимости) |
| Новое поле стиля | `style_config.STYLE_FIELD_KEYS` + `ParagraphStyleSpec` + `_apply_paragraph_spec` |
| Паритет FODT/LaTeX | `md2fodt/emitter.py` / `md2latex/emitter.py` |
---
*Файл сгенерирован по состоянию исходников репозитория (пакет 0.1.0). Пользовательские howto — в `docs/`. Этот файл — инвентарь возможностей и кода.*
-74
View File
@@ -1,74 +0,0 @@
# md2gost
Скрипт для генерации docx отчетов в соответствии с ГОСТ из markdown файла.
## Основные возможности
- Генерация отчета;
- ~~Добавление титульной страницы в формате docx~~(not yet);
- Генерация ~~интерактивного~~(not yet) содержания;
- Поддержка сквозной нумерации ~~и кросс-референсинга~~(not yet);
- Автоматическая расстановка картинок, продолжений таблиц и листингов и т.д.
## Пример
Markdown-файл: [example.md](https://github.com/witelokk/md2gost/blob/main/examples/example.md).
Сгенерированный файл в zip архиве (комманда `python -m md2gost --syntax-highlighting example.md`): [example.zip](https://nightly.link/witelokk/md2gost/workflows/example-generator/main/example.zip?h=f65c99d31a9379f44fcc6e923de4a735a271d5aa).
## Установка
```bash
pip install --upgrade git+https://github.com/witelokk/md2gost.git@main
```
Если ваша система приняла стандарт [PEP 668](https://peps.python.org/pep-0668/), используйте [pipx](https://pypa.github.io/pipx/):
```bash
pipx install git+https://github.com/witelokk/md2gost.git@main
```
## Использование
```
(python -m ) md2docx [-h] [-o OUTPUT] [-t TEMPLATE] [--syntax-highlighting | --no-syntax-highlighting] [--debug] filename
```
При отсутствии флага -o, сгенерированый отчет будет иметь имя с названием исходного файла и расширением .md.
## Фичи
### Подписи рисунков, листингов, таблиц
Рисунки:
```markdown
![](path/to/image "Caption text")
```
Таблицы:
```markdown
%uniquename Caption text
| a | b | c |
|---|---|---|
| a | b | c |
```
Листинги:
~~~markdown
%uniquename Caption text
```python
print("hello world")
```
~~~
`uniquename` - уникальное имя для ссылок.
### Заголовки для основных разделов
Для того чтобы у заголовка не было сквозной нумерации (например для заголовка Содержание), используйте
```markdown
# *Содержание
```
### Генерация содержания
```markdown
# *Содержание
[TOC]
```
### Подсветка синтаксиса в листингах
Используйте флаг ```--syntax-highlighting```
+26 -197
View File
@@ -1,209 +1,38 @@
# GhostEditor # GhostEditor / md2gost
Программа для конвертации из формата `md` в формат `docx`(x это без макросов, m с макросами) (`Office Open XML`),`odt`(`OpenDocument`)
#### **1. python-docx** Конвертер **Markdown → DOCX** по методическим указаниям РТУ МИРЭА (ГОСТ 7.32-2017) и профилям ПИС / АПИД. Есть GUI, CLI, импорт **Word → Markdown** (`word2md`) и промпты для генерации `.md` через ИИ.
**Назначение**: Создание и модификация Word-документов.
**Ключевые возможности**:
- Добавление/редактирование текста, таблиц, изображений.
- Управление стилями, колонтитулами, сносками.
- Работа с XML через абстракции (Paragraph, Run, Table).
**Пример**: ## Быстрый старт
```python
from docx import Document
from docx.shared import Pt
doc = Document() ```bash
paragraph = doc.add_paragraph("Hello ") pip install -e .
run = paragraph.add_run("OOXML!") python -m md2gost # GUI
run.bold = True python -m md2gost report.md -o report.docx --type coursework --check
run.font.size = Pt(14) python -m word2md report.docx -o report.md
# Таблица
table = doc.add_table(rows=2, cols=2)
table.cell(0, 0).text = "Cell A1"
doc.save("report.docx")
``` ```
**Плюсы**: Windows: `build-exe.bat` → `dist\md2gost.exe` (двойной клик — GUI).
- Интуитивный API, близкий к логике Word.
- Поддержка всех ключевых частей пакета (headers, footers, styles).
**Минусы**: ## Документация
- Нет поддержки макросов.
- Ограниченная работа с графикой (изображения добавляются, но редактирование сложно).
**Ссылка**: [python-docx.readthedocs.io](https://python-docx.readthedocs.io/) Полная карта проекта (что уже умеет, пайплайны, модули): [`PROJECT.md`](PROJECT.md).
--- Пользовательская документация — в [`docs/`](docs/):
#### **2. docxtpl** | Раздел | Содержание |
**Назначение**: Генерация документов по шаблонам (Jinja2). |--------|------------|
**Особенности**: | [Быстрый старт](docs/quickstart.md) | Установка, GUI, типы документов |
- Шаблоны .docx с переменными (`{{ variable }}`) и логикой (`{% if %}`). | [Синтаксис Markdown](docs/markdown.md) | Диалект md2gost vs обычный MD |
- Поддержка таблиц, изображений, HTML-фрагментов. | [Схемы и диаграммы](docs/schemes.md) | PlantUML, Mermaid, IDEF0, DFD (Graphviz вшитый/Kroki), `md2gost.schemes.json` |
| [Типы работ](docs/types.md) | coursework, practice, vkr, PIS, APID |
| [CLI и GUI](docs/cli-gui.md) | Флаги и где они в окне |
| [Импорт Word](docs/word2md.md) | DOCX → диалект md2gost |
| [Промпты для ИИ](docs/prompts.md) | Генерация MD, кнопки схем в GUI |
**Пример**: Краткая справка по пакету: [`md2gost/README.md`](md2gost/README.md).
```python Промпты: [`prompts/`](prompts/).
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")
```
**Плюсы**: - FODT (LibreOffice): `python -m md2fodt` — [`md2fodt/`](md2fodt/)
- Идеален для шаблонизированных отчетов (договоры, счета). - PDF через LaTeX: [`md2latex/`](md2latex/)
- Интеграция с 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/) (полная спецификация).
-27
View File
@@ -1,27 +0,0 @@
from docx import Document
doc = Document(r"g:\Sync_File\Семестр 3\АИИР\КР\КР_out.docx")
print("=== biblio-ish ===")
for i, p in enumerate(doc.paragraphs):
t = p.text.strip()
style = p.style.name if p.style else None
if "ИСПОЛЬЗОВАНН" in t.upper():
print(i, style, t[:80])
if style == "Bibliography" or (t[:2] in {f"{n}." for n in range(1, 10)} and len(t) > 50):
print(i, style, t[:100])
print("=== double spaces in captions ===")
for p in doc.paragraphs:
if p.style and "Caption" in (p.style.name or "") and " " in p.text:
print(repr(p.text[:90]))
print("=== sections ===")
for i, s in enumerate(doc.sections):
has_page = "PAGE" in s.footer._element.xml
print(i, "PAGE field", has_page)
print("=== Heading 1 ===")
for p in doc.paragraphs:
if p.style and p.style.name == "Heading 1":
print(repr(p.text[:100]))
-115
View File
@@ -1,115 +0,0 @@
import pypandoc
from docx import Document
from docx.shared import Pt, Cm
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.style import WD_STYLE_TYPE
def apply_custom_formatting(doc):
"""Применяет дополнительное форматирование, которое не может быть обработано Pandoc."""
# Нумерация страниц (простой пример)
# Более сложные колонтитулы лучше настроить в reference.docx
for section in doc.sections:
section.footer.paragraphs[0].text = "\t" # Чтобы номер был по центру или справа, настройте табуляцию
# Добавление рамок к листингам кода, если они не были применены стилем
for paragraph in doc.paragraphs:
if 'листинг' in paragraph.text.lower() or 'listing' in paragraph.text.lower():
# Здесь можно применить стиль, который вы создали, например 'CodeListing'
# paragraph.style = 'CodeListing'
pass # Логика для добавления границ, если нужно
def markdown_to_docx(md_file, docx_file, reference_doc):
"""
Основная функция для конвертации Markdown в DOCX.
"""
# Шаг 1: Используем Pandoc для основной конвертации
pypandoc.convert_file(
md_file,
'docx',
outputfile=docx_file,
extra_args=[f'--reference-doc={reference_doc}', '--toc'] # --toc для создания оглавления
)
# Шаг 2: Открываем созданный документ с помощью python-docx для дальнейшей обработки
doc = Document(docx_file)
# Шаг 3: Вставляем титульный лист в начало документа
# Создаем новый пустой документ для титульного листа
title_doc = Document()
# Добавляем содержимое основного документа после титульного листа
for element in doc.element.body:
title_doc.element.body.append(element)
doc = title_doc
# Шаг 4: Применяем дополнительное форматирование
apply_custom_formatting(doc)
# Шаг 5: Сохраняем финальный документ
doc.save(docx_file)
print(f"Документ '{docx_file}' успешно создан.")
if __name__ == '__main__':
# --- НАСТРОЙКИ ---
markdown_input = 'my_report.md' # Имя вашего входного Markdown файла
docx_output = 'my_report.docx' # Имя выходного DOCX файла
reference_docx = 'samost1.docx' # Имя вашего эталонного документа
# Создаем пример Markdown файла для демонстрации
markdown_content = """
# Введение
Это введение в мою курсовую работу. Текст должен быть отформатирован с использованием стиля "Обычный".
# 1. Структура учебной работы
Здесь описывается структура работы.
## 1.1 Титульный лист
Титульный лист - это первая страница работы.
## 1.2 Содержание
Содержание включает в себя перечень всех разделов.
# 2. Оформление текста документа
Здесь описываются правила оформления текста.
- Первое правило
- Второе правило
- Третье правило
1. Нумерованный список 1
2. Нумерованный список 2
Листинг 1.1 – Пример кода
` ` `python
def hello_world():
print("Hello, World!")
` ` `
Таблица 1.1 - Пример таблицы
| Заголовок 1 | Заголовок 2 |
|-------------|-------------|
| Ячейка 1 | Ячейка 2 |
| Ячейка 3 | Ячейка 4 |
# Заключение
Это заключение.
"""
with open(markdown_input, 'w', encoding='utf-8') as f:
f.write(markdown_content)
# --- ЗАПУСК КОНВЕРТЕРА ---
markdown_to_docx(markdown_input, docx_output, reference_docx)
BIN
View File
Binary file not shown.
-45
View File
@@ -1,45 +0,0 @@
# Введение
Это введение в мою курсовую работу. Текст должен быть отформатирован с использованием стиля "Обычный".
# 1. Структура учебной работы
Здесь описывается структура работы.
## 1.1 Титульный лист
Титульный лист - это первая страница работы.
## 1.2 Содержание
Содержание включает в себя перечень всех разделов.
# 2. Оформление текста документа
Здесь описываются правила оформления текста.
- Первое правило
- Второе правило
- Третье правило
1. Нумерованный список 1
2. Нумерованный список 2
Листинг 1.1 – Пример кода
` ` `python
def hello_world():
print("Hello, World!")
` ` `
Таблица 1.1 - Пример таблицы
| Заголовок 1 | Заголовок 2 |
|-------------|-------------|
| Ячейка 1 | Ячейка 2 |
| Ячейка 3 | Ячейка 4 |
# Заключение
Это заключение.
BIN
View File
Binary file not shown.
+147
View File
@@ -0,0 +1,147 @@
# ai/ — промпты и документация для нейросетей
Материалы md2gost, переписанные под работу с языковыми моделями.
**Главное правило: файл из `ai/prompts/` — это целиком промпт.** В нём нет ни заголовка «скопируйте текст ниже», ни разделителей, ни инструкций для человека. Открыл файл, выделил всё, вставил в модель — работает. Инструкции для человека живут только в этом README.
Файлы из `ai/context/` — справочники: их дают модели как дополнительный контекст, когда нужна точность в узком месте. Файлы из `ai/examples/` — эталонные документы, проходящие проверку `--check` без замечаний; их дают как образец.
---
## 1. Что где лежит
### `ai/prompts/` — готовые к вставке промпты
| Файл | Когда брать | Размер |
|------|-------------|--------|
| [`core-md2gost.md`](prompts/core-md2gost.md) | нужен просто markdown в диалекте md2gost, без привязки к типу учебной работы | 12 тыс. знаков |
| [`report-mirea.md`](prompts/report-mirea.md) | отчёт по практике, курсовая, ВКР (`practice`, `coursework`, `vkr`) | 12 тыс. знаков |
| [`report-apid.md`](prompts/report-apid.md) | курсовая «Архитектура приложений и данных» (`APID_coursework`) | 10 тыс. знаков |
| [`report-pis.md`](prompts/report-pis.md) | итоговый отчёт по практическим работам (`PIS_custom`) | 7 тыс. знаков |
| [`voice-human.md`](prompts/voice-human.md) | добавка к любому из промптов выше: живой текст про предмет, а не отчёт модели о сдаче задания | 5 тыс. знаков |
| [`review-fix.md`](prompts/review-fix.md) | есть готовый `.md`, надо починить его под ТЗ (в том числе по выводу `--check`) | 8 тыс. знаков |
| [`from-word.md`](prompts/from-word.md) | есть результат импорта `word2md`, надо довести до диалекта | 6 тыс. знаков |
Промпты самодостаточны: правила диалекта в них повторяются намеренно, чтобы каждый работал в одиночку.
### `ai/context/` — справочники для догрузки
| Файл | О чём | Размер |
|------|-------|--------|
| [`syntax.md`](context/syntax.md) | полная спецификация диалекта: регулярные выражения, приоритеты, подводные камни | 16 тыс. знаков |
| [`diagrams.md`](context/diagrams.md) | все нотации схем с готовыми каркасами: PlantUML, C4, Mermaid, IDEF0, DFD | 16 тыс. знаков |
| [`archimate.md`](context/archimate.md) | ArchiMate 3.2: макросы элементов и связей, направления, viewpoints, шаблоны | 22 тыс. знаков |
| [`checker.md`](context/checker.md) | каталог замечаний `--check` с текстами сообщений и способами починки | 10 тыс. знаков |
| [`cli.md`](context/cli.md) | все ключи запуска, профили типов, конфигурационные файлы, оформление | 12 тыс. знаков |
### `ai/examples/` — эталоны
| Файл | Тип | Что демонстрирует |
|------|-----|-------------------|
| [`coursework.md`](examples/coursework.md) | `coursework` | полная структура, IDEF0, C4, ER-диаграмма, объединение ячеек, формула, листинг, два приложения, шесть источников |
| [`pis-report.md`](examples/pis-report.md) | `PIS_custom` | четыре практические работы, DFD, IDEF0, сквозные метки, расчёт нагрузки |
Оба файла проверены: `--check-only` не даёт ни одного замечания, сборка DOCX проходит на локальном рендере без интернета.
---
## 2. Рецепты
Что вставлять в модель под конкретную задачу.
| Задача | Что даёшь модели |
|--------|------------------|
| Сгенерировать курсовую или отчёт по практике | `prompts/report-mirea.md` → вторым сообщением `prompts/voice-human.md` → третьим сообщением входные данные |
| То же, но нужен упор на схемы | добавь `context/diagrams.md` перед входными данными |
| Курсовая АПИД | `prompts/report-apid.md` + `prompts/voice-human.md` + при необходимости `context/archimate.md` |
| Отчёт по практическим работам ПИС | `prompts/report-pis.md` + входные данные |
| Дописать раздел в существующий отчёт | `prompts/core-md2gost.md` + `prompts/voice-human.md` + текущий `.md` целиком + что дописать |
| Починить файл по замечаниям проверки | `prompts/review-fix.md` + вывод `--check` + `.md` целиком |
| Довести импорт из Word | `prompts/from-word.md` + результат `word2md` |
| Нарисовать одну схему | `context/diagrams.md` (или `context/archimate.md`) + описание того, что надо изобразить |
| Разобраться, почему конвертер ругается | `context/checker.md` + текст ошибки |
| Модель путается в синтаксисе | добавь `context/syntax.md` |
| Нужен образец «как должно выглядеть» | добавь `examples/coursework.md` |
Если контекст модели ограничен, порядок приоритета такой: промпт → входные данные → эталон → справочник. Справочник — первое, чем можно пожертвовать.
---
## 3. Шаблон входных данных
Следующим сообщением после промпта дай модели заполненный блок:
```text
Тип работы: coursework
Тема: Разработка модуля учёта заявок на техническое обслуживание
Год сдачи: 2026
Дисциплина: Проектирование информационных систем
Объект: процесс приёма и исполнения заявок на обслуживание оборудования
Что уже известно:
- склад обрабатывает около 400 отгрузок в сутки
- заявки принимаются по телефону, фиксируются в электронных таблицах
- нужна интеграция со справочником оборудования по REST
Требования руководителя:
- обязательны диаграмма прецедентов и модель данных
- три раздела основной части
Литература: если есть свой список — приведи его здесь; иначе модель подберёт сама
Черновик: приложи имеющийся текст, если он есть
```
Чем конкретнее «что уже известно», тем меньше выдумок в тексте. Числа, названия и ограничения — самое ценное, что можно дать.
---
## 4. Полный цикл работы
```bash
# 1. Сохранить ответ модели как report.md
# 2. Проверить по ТЗ
python -m md2gost report.md --check-only --type coursework
# 3. Если есть замечания — отдать их модели вместе с файлом
# и промптом ai/prompts/review-fix.md, сохранить результат, вернуться к шагу 2
# 4. Собрать документ
python -m md2gost report.md -o report.docx --type coursework --check --strict
# 5. Проверить вёрстку готового файла (Windows + Word + pywin32)
python -m md2gost report.docx --check-pages
```
Автотитул при необходимости: положить рядом `info_conv.yaml`, добавить в начало `.md` блок ` ```title ` с номером работы и годом, запустить с `--auto-title --student "…" --group "…"`. Подробности — в [`context/cli.md`](context/cli.md).
---
## 5. Куда вставлять промпт
| Инструмент | Куда |
|------------|------|
| ChatGPT, Claude | системная инструкция проекта либо первое сообщение диалога |
| Cursor | правила для ИИ, либо первое сообщение агенту |
| GitHub Copilot Chat | контекст чата |
| API | роль `system` |
Промпты рассчитаны на модель с рассуждением и длинным контекстом: они содержат чек-листы самопроверки, которые слабые модели игнорируют. Если результат не проходит `--check` с первого раза — это нормальный рабочий цикл, для него и сделан `review-fix.md`.
---
## 6. Что модель делать не должна
Проверка `--check` не ловит фактические ошибки и выдуманные источники. Перед сдачей обязательно вычитай:
- **список литературы** — модели склонны придумывать издания, ISBN и число страниц. Промпты запрещают это прямо, но проверять всё равно нужно;
- **числа и названия** — если ты их не давал, модель их сочинила;
- **соответствие теме** — структура может быть безупречной при пустом содержании.
---
## 7. Отношение к остальному репозиторию
Каталоги `docs/` и `prompts/` в корне репозитория остаются как есть: на них ссылается встроенная справка приложения (**Справка → Документация**, **Справка → Промпт для ИИ**). Каталог `ai/` — самостоятельный набор, переписанный под подачу в модели, и ничего в приложении не ломает.
Источник истины по возможностям конвертера — код в `md2gost/`; справочники в `ai/context/` собраны по нему, а не по старой документации.
+514
View File
@@ -0,0 +1,514 @@
# ArchiMate 3.2 для md2gost — рабочий справочник
Как корректно генерировать диаграммы ArchiMate® 3.2 в PlantUML для отчётов md2gost. Версия языка — только **3.2**. ArchiMate® — товарный знак The Open Group; этот текст — учебная выжимка, не копия спецификации.
---
## 1. Как рисовать
Оградка ` ```uml-archimate ` или ` ```archimate `. В теле — **только макросы**: ни `@startuml`, ни `@enduml`, ни `!include` (их подставит схема).
~~~markdown
Слоистая архитектура портала приведена на @Рисунок:arch_layered.
%arch_layered Слоистая архитектура портала +landscape
```uml-archimate
LAYOUT_TOP_DOWN()
Business_Actor(student, "Студент")
Business_Process(enroll, "Подача заявки")
Application_Service(portalSvc, "Сервис приёма заявок")
Application_Component(portal, "Веб-портал")
Rel_Assignment(student, enroll)
Rel_Serving(portalSvc, enroll)
Rel_Realization(portal, portalSvc)
```
~~~
Широкие схемы (Layered, Application Cooperation) почти всегда с `+landscape`. Флаг `+listing` добавит исходник схемы отдельным Листингом.
Формат макроса: `Категория_Элемент(alias, "Подпись")`. Alias — латиница (`student`, `crm`); нельзя `end`, `start`, `group`. Подписи русские, в кавычках; длинные переносятся через `\n`.
---
## 2. Каркас языка
**Три core-слоя:**
| Слой | Что показывает |
|------|----------------|
| Business | бизнес-сервисы для клиентов, процессы и акторы, которые их реализуют |
| Application | прикладные сервисы и приложения, поддерживающие бизнес |
| Technology | обработка, хранение, сети, а также физический мир |
Плюс слой Strategy, слой Implementation & Migration и аспект Motivation.
**Четыре аспекта:**
| Аспект | Роль | Примеры |
|--------|------|---------|
| Active structure | кто действует | Actor, Role, Component, Node, Device |
| Behavior | что делается | Process, Function, Interaction, Service, Event |
| Passive structure | над чем работают | Business Object, Data Object, Artifact |
| Motivation | зачем | Stakeholder, Driver, Goal, Requirement |
**Каноническая цепочка внутри слоя:**
```text
Active structure —Assignment→ internal behavior (Process / Function)
internal behavior —Realization→ Service (внешнее поведение)
Service —Serving→ потребитель (Role / Process / Component)
behavior —Access→ passive structure
```
**Два межслойных моста:** Serving (сервис предоставляет функциональность потребителю) и Realization (более конкретное реализует более абстрактное).
---
## 3. Элементы → макросы
### Motivation
| Элемент | Макрос | Определение |
|---------|--------|-------------|
| Stakeholder | `Motivation_Stakeholder` | роль лица или организации, заинтересованной в эффектах архитектуры |
| Driver | `Motivation_Driver` | условие, побуждающее ставить цели и вносить изменения |
| Assessment | `Motivation_Assessment` | результат анализа положения дел относительно driver |
| Goal | `Motivation_Goal` | высокоуровневое намерение, желаемое состояние |
| Outcome | `Motivation_Outcome` | конечный результат, эффект |
| Principle | `Motivation_Principle` | общее свойство, применимое к любой системе в контексте |
| Requirement | `Motivation_Requirement` | требуемое свойство конкретной системы |
| Constraint | `Motivation_Constraint` | ограничение на архитектуру или внедрение |
| Meaning | `Motivation_Meaning` | интерпретация концепта в контексте |
| Value | `Motivation_Value` | относительная ценность концепта |
### Strategy
| Элемент | Макрос | Определение |
|---------|--------|-------------|
| Resource | `Strategy_Resource` | актив, которым владеют или управляют |
| Capability | `Strategy_Capability` | способность активной структуры |
| Value Stream | `Strategy_ValueStream` | последовательность активностей, создающая результат для клиента |
| Course of Action | `Strategy_CourseOfAction` | план настройки capabilities и resources под цель |
### Business
| Элемент | Макрос | Аспект | Определение |
|---------|--------|--------|-------------|
| Business Actor | `Business_Actor` | active | сущность, способная выполнять поведение |
| Business Role | `Business_Role` | active | ответственность за поведение |
| Business Collaboration | `Business_Collaboration` | active | агрегат двух и более активных структур |
| Business Interface | `Business_Interface` | active | точка доступа к бизнес-сервису |
| Business Process | `Business_Process` | behavior | последовательность поведения к результату |
| Business Function | `Business_Function` | behavior | набор поведения по компетенциям и ресурсам |
| Business Interaction | `Business_Interaction` | behavior | коллективное поведение участников |
| Business Event | `Business_Event` | behavior | изменение состояния на бизнес-уровне |
| Business Service | `Business_Service` | behavior | явно определённое поведение, доступное среде |
| Business Object | `Business_Object` | passive | концепт предметной области |
| Contract | `Business_Contract` | passive | соглашение провайдера и потребителя |
| Representation | `Business_Representation` | passive | форма представления информации объекта |
| Product | `Business_Product` | composite | набор сервисов и объектов вместе с контрактом |
### Application
| Элемент | Макрос | Аспект | Определение |
|---------|--------|--------|-------------|
| Application Component | `Application_Component` | active | модульная инкапсуляция прикладной функциональности |
| Application Collaboration | `Application_Collaboration` | active | агрегат прикладных активных структур |
| Application Interface | `Application_Interface` | active | точка доступа к прикладным сервисам |
| Application Function | `Application_Function` | behavior | автоматизированное поведение компонента |
| Application Interaction | `Application_Interaction` | behavior | коллективное поведение компонентов |
| Application Process | `Application_Process` | behavior | последовательность прикладного поведения |
| Application Event | `Application_Event` | behavior | изменение состояния приложения |
| Application Service | `Application_Service` | behavior | внешне видимое прикладное поведение |
| Data Object | `Application_DataObject` | passive | данные, структурированные для обработки |
### Technology и Physical
| Элемент | Макрос | Определение |
|---------|--------|-------------|
| Node | `Technology_Node` | вычислительный или физический ресурс, хостящий другие |
| Device | `Technology_Device` | физический ИТ-ресурс |
| System Software | `Technology_SystemSoftware` | ПО среды исполнения или хранения |
| Technology Collaboration | `Technology_Collaboration` | агрегат технологических активных структур |
| Technology Interface | `Technology_Interface` | точка доступа к технологическим сервисам |
| Path | `Technology_Path` | связь между технологическими структурами для обмена |
| Communication Network | `Technology_CommunicationNetwork` | структуры передачи данных |
| Technology Function | `Technology_Function` | набор технологического поведения |
| Technology Process | `Technology_Process` | последовательность технологического поведения |
| Technology Interaction | `Technology_Interaction` | коллективное технологическое поведение |
| Technology Event | `Technology_Event` | изменение технологического состояния |
| Technology Service | `Technology_Service` | внешне видимое технологическое поведение |
| Artifact | `Technology_Artifact` | данные или файл, используемый при разработке и эксплуатации |
| Equipment | `Physical_Equipment` | физические машины и инструменты |
| Facility | `Physical_Facility` | физическая структура или окружение |
| Distribution Network | `Physical_DistributionNetwork` | сеть транспортировки материи или энергии |
| Material | `Physical_Material` | осязаемая материя или энергия |
В 3.2 Device, System Software, Facility и Equipment — **не** подтипы Node, а технологические активные структуры, связанные с Node композицией или агрегацией.
### Implementation & Migration
| Элемент | Макрос | Определение |
|---------|--------|-------------|
| Work Package | `Implementation_WorkPackage` | серия действий с результатом в сроках и ресурсах |
| Deliverable | `Implementation_Deliverable` | определённый результат work package |
| Implementation Event | `Implementation_Event` | изменение состояния при внедрении |
| Plateau | `Implementation_Plateau` | стабильное состояние архитектуры на период |
| Gap | `Implementation_Gap` | различие между двумя plateau |
### Composite и Junction
| Элемент | Макрос |
|---------|--------|
| Grouping | `Grouping` / `Other_Grouping` / `Group` |
| Location | `Other_Location` / `Business_Location` |
| And Junction | `Junction_And` |
| Or Junction | `Junction_Or` |
Grouping и Junction допустимы в любом viewpoint.
### Какой элемент поведения выбрать
| Нужно показать | Берётся |
|----------------|---------|
| последовательность шагов к результату | `*_Process` |
| группу поведения по компетенциям и ресурсам | `*_Function` |
| поведение двух и более участников | `*_Interaction` |
| внешне видимое поведение, чёрный ящик | `*_Service` |
| момент или смену состояния | `*_Event` |
Слой задаётся типом элемента, а не подписью: `Business_Process` ≠ `Application_Process` ≠ `Technology_Process`.
---
## 4. Связи
| Тип | Класс | Смысл | Макрос | Нотация |
|-----|-------|-------|--------|---------|
| Composition | structural | целое состоит из частей, часть не живёт отдельно | `Rel_Composition` | `*--` |
| Aggregation | structural | целое объединяет части, части живут отдельно | `Rel_Aggregation` | `o--` |
| Assignment | structural | ответственность, исполнение, размещение | `Rel_Assignment` | `@-->>` |
| Realization | structural | конкретное реализует абстрактное | `Rel_Realization` | `~~\|>` |
| Serving | dependency | предоставляет функциональность другому | `Rel_Serving` | `-->` |
| Access | dependency | поведение работает с пассивным элементом | `Rel_Access`, `_r`, `_w`, `_rw` | `~~` |
| Influence | dependency | влияет на достижение motivation-элемента | `Rel_Influence` | `..>` |
| Triggering | dynamic | временная или причинная активация | `Rel_Triggering` | `-->>` |
| Flow | dynamic | передача чего-либо | `Rel_Flow` | `..>>` |
| Specialization | other | является разновидностью | `Rel_Specialization` | `--\|>` |
| Association | other | неспецифичная связь | `Rel_Association` | `--` |
Направление раскладки: суффиксы `_Up`, `_Down`, `_Left`, `_Right` — например `Rel_Serving_Up(svc, proc)`.
### Направления связей — где чаще всего ошибаются
| Связь | Откуда → куда |
|-------|---------------|
| Serving | поставщик (обычно Service) → **потребитель** |
| Realization | более конкретное → более абстрактное |
| Composition, Aggregation | целое → часть |
| Assignment | активная структура → поведение |
| Access | поведение → **пассивный** элемент, вид read / write / read-write |
| Triggering | предшественник → следующий |
| Flow | источник → приёмник |
| Influence | влияющий → motivation-элемент, сила в подписи (`++`, `+`, `0`, `-`, `--`) |
| Specialization | специализация → общий тип |
### Правила метамодели
- Элемент может композировать или агрегировать элементы того же класса.
- Внутренние поведения одного слоя могут агрегировать друг друга (Function агрегирует Process).
- Collaboration ведёт себя как соответствующая внутренняя активная структура.
- Stakeholder связывается Association с любым элементом; остальные motivation-элементы влияют друг на друга через Influence.
- Association формально возможна между любыми классами, но применяется только когда нет более точного типа.
---
## 5. Канонические цепочки
```text
# Внутри слоя Business
Business_Actor / Business_Role Rel_Assignment Business_Process / Function
Business_Process Rel_Realization Business_Service
Business_Process Rel_Access_* Business_Object
Business_Event Rel_Triggering Business_Process
# Внутри слоя Application
Application_Component Rel_Assignment Application_Function / Process
Application_Function Rel_Realization Application_Service
Application_Service Rel_Serving Business_Process / Business_Role
Application_Component Rel_Access_* Application_DataObject
Application_Component Rel_Composition Application_Component
# Технологии и данные
Technology_Node Rel_Composition Technology_Device / SystemSoftware
Technology_* Rel_Assignment Technology_Function / Process
Technology_Function Rel_Realization Technology_Service
Technology_Service Rel_Serving Application_Component
Technology_Artifact Rel_Realization Application_DataObject
Application_DataObject Rel_Realization Business_Object
# Стратегия и мотивация
Business_Process / App_Service Rel_Realization Strategy_Capability
Application_Service / Component Rel_Realization Motivation_Requirement
Motivation_Driver Rel_Association Motivation_Stakeholder
Motivation_Assessment Rel_Association Motivation_Driver
Motivation_Goal Rel_Realization Motivation_Outcome
# Внедрение и миграция
Implementation_WorkPackage Rel_Realization Implementation_Deliverable
Implementation_Plateau Rel_Aggregation элементы core-слоёв
Implementation_Gap Rel_Association между двумя plateau
```
### Запреты
1. Не `Rel_Assignment` от Data Object или Business Object к процессу.
2. Не `Rel_Realization` от Component сразу к Process: нужен внутренний Function или Process, затем Realization к Service.
3. Не `Rel_Serving` в направлении «потребитель → сервис».
4. Не подменять Access потоком Flow только потому, что речь о данных.
5. Не ставить `Business_Process` там, где исполнитель — приложение.
6. Не ставить Association на все рёбра «на всякий случай».
7. Не считать Device подтипом Node без композиции или агрегации.
---
## 6. Viewpoints
**Один рисунок = один viewpoint.** Не сваливать мотивацию, бизнес, приложения и инфраструктуру на один холст (Layered — исключение, но и там ограничивай число элементов).
| Viewpoint | Фокус | Типичные элементы |
|-----------|-------|-------------------|
| Organization | структура организационных единиц | Actor, Role, Collaboration, Location |
| Application Structure | внутренняя структура приложений | Component, Interface, Data Object |
| Information Structure | данные и информация | Business Object, Representation, Data Object, Artifact |
| Technology | инфраструктура | Node, Device, System Software, Network, Path |
| Layered | обзор слоёв | Business + Application + Technology, Serving и Realization |
| Physical | физический мир | Equipment, Facility, Distribution Network, Material |
| Product | ценность продукта | Product, Contract, Service, Interface |
| Application Usage | приложения в бизнес-контексте | бизнес-процессы + прикладные сервисы |
| Technology Usage | технологии для приложений | Application + Technology services и nodes |
| Business Process Cooperation | связь процессов | Process, Event, Service, Role |
| Application Cooperation | связь приложений | Component, Service, Interface, Data, Flow |
| Service Realization | как реализованы сервисы | Service ← Process / Function ← активная структура |
| Implementation and Deployment | развёртывание | Application + Artifact + Node / Device |
| Stakeholder, Goal Realization, Requirements Realization, Motivation | мотивация | Driver, Goal, Outcome, Requirement, Constraint |
| Strategy, Capability Map, Value Stream, Resource Map | стратегия | Capability, Resource, Value Stream, Course of Action |
| Project, Migration, Implementation and Migration | внедрение | Work Package, Deliverable, Plateau, Gap |
### Макет
- Структурные схемы: `LAYOUT_TOP_DOWN()` или вложенность через `{ }`.
- Процессы слева направо: `LAYOUT_LEFT_RIGHT()` и `Rel_*_Right`.
- Layered: бизнес сверху, технологии снизу; Serving и Realization направлены вверх (`Rel_Serving_Up`).
- Выравнивание скрытыми связями: `Lay_U`, `Lay_D`, `Lay_L`, `Lay_R`.
- Вложение элементов:
```text
Business_Product(prod, "Тариф") {
Business_Service(s1, "Подключение")
Business_Contract(c1, "Оферта")
}
```
Для Service, Actor, Value и ValueStream при вложении добавляется `$nest=%true()`.
---
## 7. Готовые шаблоны
Тела для ` ```uml-archimate ` без обёртки.
### Organization
```text
LAYOUT_TOP_DOWN()
Business_Actor(uni, "Университет") {
Business_Actor(dean, "Деканат")
Business_Actor(it, "Управление ИТ")
}
Business_Role(student, "Студент")
Business_Role(clerk, "Методист")
Rel_Assignment(dean, clerk)
Rel_Assignment(uni, student)
```
### Service Realization
```text
LAYOUT_TOP_DOWN()
Business_Role(applicant, "Абитуриент")
Business_Process(apply, "Подача заявления")
Business_Service(applySvc, "Приём заявлений")
Application_Service(webSvc, "Веб-приёмная")
Application_Component(lk, "Личный кабинет")
Application_Function(submitFn, "Отправка формы")
Rel_Assignment(applicant, apply)
Rel_Realization(apply, applySvc)
Rel_Serving(webSvc, apply)
Rel_Assignment(lk, submitFn)
Rel_Realization(submitFn, webSvc)
```
### Application Cooperation
```text
LAYOUT_LEFT_RIGHT()
Application_Component(crm, "CRM")
Application_Component(billing, "Биллинг")
Application_Service(crmApi, "API клиентов")
Application_DataObject(client, "Карточка клиента")
Rel_Realization(crm, crmApi)
Rel_Serving(crmApi, billing)
Rel_Access_rw(crm, client)
Rel_Access_r(billing, client)
Rel_Flow(crm, billing, "событие оплаты")
```
### Layered
```text
LAYOUT_TOP_DOWN()
Business_Process(sale, "Оформление заказа")
Business_Object(order, "Заказ")
Application_Service(shopSvc, "Интернет-магазин")
Application_Component(shop, "ShopApp")
Technology_Service(dbSvc, "СУБД")
Technology_Node(dbHost, "Сервер БД")
Technology_SystemSoftware(pg, "PostgreSQL")
Rel_Access_w(sale, order)
Rel_Serving_Up(shopSvc, sale)
Rel_Realization(shop, shopSvc)
Rel_Serving_Up(dbSvc, shop)
Rel_Assignment(dbHost, dbSvc)
Rel_Composition(dbHost, pg)
```
### Requirements Realization
```text
LAYOUT_TOP_DOWN()
Motivation_Stakeholder(rector, "Ректор")
Motivation_Driver(comp, "Конкуренция вузов")
Motivation_Goal(digital, "Цифровизация приёма")
Motivation_Requirement(online, "Заявление онлайн круглосуточно")
Motivation_Constraint(fz152, "152-ФЗ")
Application_Service(portal, "Портал абитуриента")
Rel_Association(rector, comp)
Rel_Association(comp, digital)
Rel_Realization(online, digital)
Rel_Influence(fz152, online, "-")
Rel_Realization(portal, online)
```
### Implementation & Migration
```text
LAYOUT_LEFT_RIGHT()
Implementation_Plateau(asIs, "AS-IS 2025")
Implementation_Plateau(toBe, "TO-BE 2026")
Implementation_Gap(gap1, "Нет единого личного кабинета")
Implementation_WorkPackage(wp, "Проект «Единый ЛК»")
Implementation_Deliverable(d1, "Релиз 1.0")
Implementation_Event(goLive, "Ввод в эксплуатацию")
Rel_Association(gap1, asIs)
Rel_Association(gap1, toBe)
Rel_Realization(wp, d1)
Rel_Triggering(d1, goLive)
Rel_Triggering(goLive, toBe)
```
### Technology Usage
```text
LAYOUT_TOP_DOWN()
Application_Component(api, "API Gateway")
Technology_Node(k8s, "Кластер Kubernetes")
Technology_Device(node1, "Рабочий узел")
Technology_SystemSoftware(os, "Linux")
Technology_CommunicationNetwork(net, "Сеть кластера")
Technology_Artifact(image, "api:1.2")
Rel_Serving_Up(k8s, api)
Rel_Composition(k8s, node1)
Rel_Composition(node1, os)
Rel_Association(node1, net)
Rel_Assignment(node1, image)
```
### Процесс с Junction
```text
LAYOUT_LEFT_RIGHT()
Business_Event(incident, "Инцидент")
Business_Process(triage, "Первичная оценка")
Junction_Or(j1, "")
Business_Process(restore, "Восстановление сервиса")
Business_Process(investigate, "Расследование")
Business_Process(close, "Закрытие инцидента")
Rel_Triggering(incident, triage)
Rel_Triggering(triage, j1)
Rel_Triggering(j1, restore)
Rel_Triggering(j1, investigate)
Rel_Triggering(restore, close)
Rel_Triggering(investigate, close)
```
---
## 8. Антипаттерны
| Плохо | Почему | Как надо |
|-------|--------|----------|
| все связи `Rel_Association` | теряется семантика | Serving, Realization, Assignment, Access |
| `Application_Component` вместо `Business_Actor` | разные аспекты и слои | Actor и Role для людей и подразделений |
| Business Process «внутри» компонента без Assignment | слои смешаны | процесс на бизнес-слое, Serving от прикладного сервиса |
| Serving от Role к Service | направление наоборот | Service → Role или Process |
| Realization Component → Process | пропущено внутреннее поведение | Component → Function → Service |
| Flow вместо Access к Data Object | Flow — передача, Access — работа с объектом | `Rel_Access_r/w/rw` |
| один Layered на 40 элементов | нечитаемо | несколько viewpoint'ов |
| C4 `Person` или UML-класс в блоке archimate | другой язык | только макросы ArchiMate |
| `Process` без указания слоя | в 3.2 слой обязателен | `Business_Process`, `Application_Process`, `Technology_Process` |
---
## 9. Чеклист генерации
1. Версия языка 3.2, макросы с указанием слоя.
2. Оградка ` ```uml-archimate `, без `@startuml` и `!include`.
3. Выбран **один** viewpoint; 8–18 элементов на рисунок.
4. Сначала объявлены все элементы, затем все `Rel_*`.
5. Alias латиницей, подписи по-русски в кавычках.
6. Serving — от сервиса к потребителю; Realization — от конкретного к абстрактному.
7. Поведение того же слоя, что и его исполнитель по Assignment.
8. Нет C4, UML-классов, BPMN-развилок и Association «по умолчанию».
9. Перед блоком стоит `%id Название`, при широкой схеме — `+landscape`.
10. В тексте есть ссылка `@Рисунок:id`.
---
## 10. Запасной вариант без stdlib
Если библиотека ArchiMate недоступна, схему можно нарисовать в ` ```uml ` низкоуровневым ключевым словом:
~~~markdown
%arch_fallback Фрагмент архитектуры
```uml
@startuml
archimate #Business "Подача заявки" as p <<business-process>>
archimate #Application "Портал" as c <<application-component>>
c --> p
@enduml
```
~~~
Смешивать макросы и низкоуровневый `archimate` на одной схеме нельзя.
+165
View File
@@ -0,0 +1,165 @@
# Проверка ТЗ — полный каталог замечаний
Что именно проверяет `md2gost … --check`, с какими текстами сообщений и как чинить каждое замечание.
---
## 1. Как запускать
```bash
python -m md2gost report.md --check-only --type coursework # только проверка
python -m md2gost report.md -o out.docx --type coursework --check --strict
```
| Ключ | Поведение |
|------|-----------|
| `--check` | проверить и напечатать отчёт, документ всё равно собрать |
| `--check-only` | только проверка, DOCX не собирается, код выхода 0 |
| `--strict` | при наличии хотя бы одной ошибки вывести `Проверка ТЗ: есть ошибки (--strict)`, код выхода 1, документ не собирать |
| `--check-pages` | отдельная эвристическая проверка вёрстки готового DOCX через Microsoft Word |
Формат отчёта:
```text
Проверка ТЗ: найдено замечаний — 3
[error] structure.missing: Отсутствует обязательный раздел «ЗАКЛЮЧЕНИЕ»
[warning] ref.unused (стр. md:42): Метка «tab_cmp» объявлена, но в тексте нет ссылки @…:tab_cmp
[error] ref.ris (стр. md:57): Используйте «Рисунок», не «рис.»
```
Без замечаний: `Проверка ТЗ: замечаний нет.`
Уровни: **error** — блокирует сборку при `--strict`; **warning** — не блокирует; **heuristic** — только у проверки вёрстки, никогда не влияет на код выхода.
---
## 2. Структура документа
| ID | Уровень | Сообщение | Причина и решение |
|----|---------|-----------|-------------------|
| `structure.missing` | error | `Отсутствует обязательный раздел «{название}»` | Нет одного из: СОДЕРЖАНИЕ, ВВЕДЕНИЕ, ЗАКЛЮЧЕНИЕ, СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ. Добавить раздел `# *НАЗВАНИЕ` с содержательным текстом. Допускается написание «СПИСОК ИСПОЛЬЗУЕМЫХ ИСТОЧНИКОВ» |
| `structure.missing` | error | `Отсутствует раздел «СОДЕРЖАНИЕ»` | Профиль `PIS_custom`: нет `# *СОДЕРЖАНИЕ` |
| `structure.toc` | warning | `Нет маркера [TOC] для автособираемого содержания` | Добавить `[TOC]` прописными отдельной строкой под `# *СОДЕРЖАНИЕ` |
| `structure.toc` | **error** | `Нет маркера [TOC] для оглавления (обязательно для итогового отчёта)` | То же, но для `PIS_custom` это ошибка |
| `structure.body` | error | `Нет нумерованного раздела основной части (# заголовок без *)` | Все H1 оказались со звёздочкой. Добавить `# 1 Название` |
| `structure.graphic` | error | `Для ВКР обязательно приложение «Графический материал»` | В тексте нет подстроки «Графический материал». Добавить `## Приложение Б Графический материал` |
| `structure.practical` | error | `Нет заголовков вида «Практическая работа №N. …» (H1 без *, каждая работа — отдельный раздел)` | Профиль `PIS_custom`: каждую работу оформить как `# Практическая работа №N. Название` |
| `structure.apid` | error | `Для КР АПИД отсутствует обязательный раздел: «{название}»` | См. таблицу ниже |
### Обязательные разделы КР АПИД
Проверка ищет подстроки в тексте заголовков (регистр не важен):
| Требуемый раздел | Что должно найтись в заголовках |
|------------------|--------------------------------|
| `1. Теоретические аспекты разработки архитектуры приложений и данных: …` | «теоретическ… аспект» |
| `2. Прикладные аспекты разработки архитектуры приложений и данных …` | «прикладн… аспект» |
| `2.1 Описание проекта команды и разрабатываемого программного приложения` | «2.1» и «описание проекта» рядом |
| `2.2 Описание роли … в приложении …` | «2.2» и «описание роли» рядом |
| `2.3 Описание архитектуры программного приложения и данных для роли` | «2.3» и «архитектур» |
| `2.4 Варианты развития архитектуры программного приложения` | «2.4» и «вариант» |
---
## 3. Заголовки
| ID | Уровень | Сообщение | Причина и решение |
|----|---------|-----------|-------------------|
| `heading.numbered_special` | error | `Спецраздел «{title}» должен быть без номера: # *{title}` | H1 со спецназванием без звёздочки. Добавить `*` сразу после `# ` |
| `heading.case` | warning | `Спецраздел лучше писать ПРОПИСНЫМИ: «{TITLE}»` | «Введение» вместо «ВВЕДЕНИЕ» |
| `heading.case` | warning | `Подраздел с прописной буквы: «{Title}»` | Профиль `PIS_custom`: заголовок уровня `##` и глубже начинается со строчной буквы |
| `heading.trailing_dot` | warning | `Заголовок без точки в конце: «{title}»` | Профиль `PIS_custom`: убрать точку в конце заголовка |
Спецразделы, требующие звёздочки на уровне H1: СОДЕРЖАНИЕ, ВВЕДЕНИЕ, ЗАКЛЮЧЕНИЕ, СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ, СПИСОК ИСПОЛЬЗУЕМЫХ ИСТОЧНИКОВ, ПРИЛОЖЕНИЕ, ПРИЛОЖЕНИЯ, а также любой H1, начинающийся на «ПРИЛОЖЕН».
---
## 4. Ссылки и терминология
| ID | Уровень | Сообщение | Причина и решение |
|----|---------|-----------|-------------------|
| `ref.ris` | error | `Используйте «Рисунок», не «рис.»` | В тексте найдено «рис.» (шаблон `\bрис\.`). Заменить на `@Рисунок:id` или слово «Рисунок» |
| `ref.tabl` | error | `Используйте «Таблица», не «табл.»` | Найдено «табл.». Заменить на `@Таблица:id` или слово «Таблица» |
| `footnote` | error | `Сноски в конце страницы не допускаются` | Найдена конструкция `[^…]`. Перенести содержание в основной текст или оформить ссылкой на источник |
| `table.nopp` | error | `Графу «№ п/п» в таблицы не включают` | Найдено «№ п/п». Удалить столбец |
| `ref.unused` | warning | `Метка «{id}» объявлена, но в тексте нет ссылки @…:{id}` | Добавить в текст перед объектом предложение со ссылкой `@Тип:{id}`, заканчивающееся точкой |
| `ref.dangling` | error | `Ссылка на несуществующую метку «{id}»` | Опечатка в `id` либо отсутствие объекта с такой меткой |
Множество меток собирается из строк `^%(\w+)` и из title картинок; множество ссылок — из `@Тип:id`. Оба множества должны совпадать.
---
## 5. Источники
| ID | Уровень | Сообщение | Причина и решение |
|----|---------|-----------|-------------------|
| `cite.intro` | error | `Во разделе «ВВЕДЕНИЕ» ссылки на источники не указываются` | Убрать `[n]` из введения; то же для заключения |
| `biblio.count` | error | `Источников должно быть от {min} до {max} (сейчас {n})` | Довести число записей до нормы профиля |
| `biblio.count` | warning | `ВКР: в разделе списка «{title}» желательно ≥10 источников (сейчас {n})` | Дополнить подраздел списка |
| `biblio.count` | warning | `ВКР: список должен делиться на разделы; в каждом ≥10 источников (сейчас всего {n}, разделов нет)` | Разбить список подразделами `##` |
| `biblio.age` | error | `Источник [{n}] старше {N} лет (год {год})` | Заменить на актуальное издание или редакцию стандарта |
| `biblio.order` | warning | `Порядок источников в списке должен совпадать с порядком первого упоминания` | Перенумеровать согласованно и в тексте, и в списке |
| `biblio.missing` | error | `Ссылка [{n}] есть в тексте, но нет в списке источников` | Добавить запись `[n]: …` |
Нормы по профилям:
| Профиль | Источников | Возраст |
|---------|-----------|---------|
| `practice`, `coursework` | 5–7 | не старше 5 лет |
| `APID_coursework` | 7–20 | не старше 5 лет |
| `vkr` | не менее 10 в каждом подразделе списка | не старше 5 лет |
| `PIS_custom` | не требуются | не старше 10 лет, если список всё же есть |
Год издания извлекается из текста записи автоматически. Если года в записи нет, проверка возраста пропускается.
---
## 6. Таблицы
| ID | Уровень | Сообщение | Причина и решение |
|----|---------|-----------|-------------------|
| `table.merge_header` | error | `Маркер склеивания ^/> нельзя использовать в первой (заголовочной) строке таблицы` | Вписать нормальный заголовок столбца |
| `table.merge_col` | error | `Маркер «>» нельзя ставить в первом столбце (нет ячейки слева)` | Убрать маркер |
| `table.continuation` | warning | `Word сам не вставит «Продолжение Таблицы N» при переносе…` | Разметка ни при чём: запускать с `--table-continuation word` (нужен Word и `pywin32`) либо `caption` |
| `listing.continuation` | warning | `Длинный листинг: Word сам не вставит «Продолжение Листинга N»…` | То же для `--listing-continuation`. Длинным считается блок кода от 40 строк — можно вынести полный код в приложение |
Замечания о продолжении выдаются только при режимах `off` и `soft`.
---
## 7. Приложения
| ID | Уровень | Сообщение | Причина и решение |
|----|---------|-----------|-------------------|
| `appendix.letter` | error | `Буква «{X}» не используется для обозначения приложений` | Запрещены Ё, З, Й, О, Ч, Ь, Ы, Ъ. Допустимы А, Б, В, Г, Д, Е, Ж, И, К, Л, М, Н, П, Р, С, Т, У, Ф, Х, Ц, Ш, Щ, Э, Ю, Я |
| `appendix.toc` | warning | `При нескольких приложениях после «ПРИЛОЖЕНИЯ» нужен перечень (основной текст) с номерами и названиями` | Добавить строки вида `Приложение А — Название` сразу после `# *ПРИЛОЖЕНИЯ` |
---
## 8. Проверка вёрстки `--check-pages`
Отдельная эвристика на готовом DOCX: ищет страницы, у которых снизу пусто больше 40 % полосы, а раздел продолжается на следующей странице.
| Ситуация | Сообщение |
|----------|-----------|
| Нет `pywin32` или Word | `Проверка вёрстки пропущена: нужен пакет pywin32 (pip install pywin32) и Microsoft Word.` |
| Word не открылся | `Проверка вёрстки пропущена: не удалось открыть Word ({ошибка}).` |
| Замечаний нет | `Проверка вёрстки (Word): замечаний по полупустым страницам нет (эвристика; возможны пропуски).` |
| Замечания есть | `Проверка вёрстки (Word): найдено замечаний — {N} (все эвристические, могут быть ложными).` |
| Конкретная страница | `[heuristic] page.fill: на стр. {N} пустой низ ~{X}% полосы, а раздел «{название}» продолжается на следующей странице…` |
Ложные срабатывания нормальны: крупный рисунок, конец пункта, намеренный разрыв. Чем помочь на уровне разметки: флаг `+landscape` у широкой схемы, перенос крупного объекта в приложение, сокращение листинга.
---
## 9. Рекомендуемый цикл доводки
```bash
python -m md2gost report.md --check-only --type coursework # 1. посмотреть замечания
# 2. отдать текст замечаний и .md модели с промптом ai/prompts/review-fix.md
python -m md2gost report.md --check-only --type coursework # 3. проверить повторно
python -m md2gost report.md -o report.docx --type coursework --check --strict
python -m md2gost report.docx --check-pages # 4. проверить вёрстку
```
Что проверка **не** ловит: смысловые ошибки, фактические неточности, выдуманные источники, слабую логику разделов и «машинный» стиль изложения. Стиль лечится промптом `ai/prompts/voice-human.md`, факты — вычиткой.
+242
View File
@@ -0,0 +1,242 @@
# Запуск, профили и настройки md2gost
Справочник по CLI, типам документов, конфигурационным файлам и оформлению. Движок один: интерфейс собирает те же параметры, что и аргументы командной строки.
---
## 1. Основные команды
```bash
python -m md2gost # окно интерфейса
python -m md2gost report.md -o report.docx --type coursework --check
python -m md2gost report.md --check-only --type vkr --strict
python -m md2gost report.docx -o report.md # импорт Word → Markdown
python -m md2gost report.docx --check-pages # проверка вёрстки готового DOCX
python -m word2md report.docx -o report.md # то же импортом напрямую
python -m md2fodt report.md -o report.fodt # экспорт в формат LibreOffice
md2gost-gui # окно интерфейса, точка входа
```
Без имени файла или с ключом `--gui` открывается окно. Windows-сборка: `build-exe.bat` даёт `dist\md2gost.exe`, двойной клик открывает окно, аргументы работают так же.
---
## 2. Типы документов `--type`
| Тип | Назначение | Нумерация объектов | Источники | Стиль |
|-----|-----------|--------------------|-----------|-------|
| `practice` (по умолчанию) | отчёт по практике | по разделам: 1.1, 2.3 | 5–7, не старше 5 лет | МИРЭА |
| `coursework` | курсовая работа | по разделам | 5–7, не старше 5 лет | МИРЭА |
| `vkr` | выпускная квалификационная работа | по разделам | не менее 10 в каждом подразделе списка | МИРЭА |
| `APID_coursework` | курсовая «Архитектура приложений и данных» | по разделам | 7–20, не старше 5 лет | МИРЭА |
| `PIS_custom` | итоговый отчёт по практическим работам | сквозная: 1, 2, 3 | не требуются | ПИС |
Что меняет профиль:
| Свойство | МИРЭА-профили | `PIS_custom` |
|----------|---------------|--------------|
| H1 | 18 пт, ПРОПИСНЫЕ, слева (СОДЕРЖАНИЕ и СПИСОК — по центру) | 14 пт, ПРОПИСНЫЕ, по центру |
| H2 / H3 | 16 пт / 14 пт | 14 пт, по ширине, отступ 1,25 см |
| Введение и заключение | обязательны | не требуются |
| Список источников | обязателен | не требуется |
| Особая структура | у `vkr` — приложение «Графический материал»; у `APID_coursework` — фиксированные заголовки разделов | H1 вида «Практическая работа №N. …» |
Общее оформление обоих пресетов: Times New Roman 14, интервал 1,5, поля 30/10/20/20 мм, абзацный отступ 1,25 см, выравнивание по ширине, подпись рисунка под ним, надписи таблицы и листинга сверху.
---
## 3. Все ключи командной строки
### Ввод и вывод
| Ключ | По умолчанию | Смысл |
|------|--------------|-------|
| `filename` | — | путь к `.md` (сборка DOCX) или `.docx` (импорт в Markdown) |
| `-o`, `--output` | рядом с исходником | путь результата |
| `-t`, `--template` | встроенный `Template.docx` | свой шаблон DOCX |
| `--gui` | — | открыть окно, можно сразу с файлом |
| `--type` | `practice` | тип документа |
### Структура и разметка
| Ключ | По умолчанию | Смысл |
|------|--------------|-------|
| `--heading-numbering {auto,manual}` | `manual` | `manual` — цифры из markdown, автонумерация Word отключена; `auto` — нумерует Word, цифры из заголовков снимаются |
| `--toc {manual,native}` | `native` | `native` — поле Word TOC (обновить при открытии); `manual` — md2gost собирает содержание с номерами страниц |
| `--emdash-to-hyphen` | выкл. | заменить все «—» на «-» |
| `--hr-pagebreak` | выкл. | строки `---`, `***`, `___` дают разрыв страницы (иначе игнорируются) |
| `--page-start N` | сквозная нумерация | начальный номер страницы в колонтитуле тела |
### Титул, задание, метаданные
| Ключ | По умолчанию | Смысл |
|------|--------------|-------|
| `--title` | — | готовый DOCX титульного листа; **отключает автогенерацию** |
| `--auto-title` | выкл. | собрать титул из `info_conv.yaml` и блока ` ```title ` |
| `--assignment` | — | DOCX бланка задания |
| `--student`, `--group` | из `md2gost.user.json` | ФИО и группа для автотитула |
| `--doc-author-from {os,student,custom}` | `os` | источник автора в свойствах DOCX |
| `--doc-author`, `--doc-last-modified-by`, `--doc-title`, `--doc-subject`, `--doc-keywords`, `--doc-comments`, `--doc-category` | — | поля свойств DOCX; пустое `--doc-comments` убирает штамп md2gost |
### Проверка
| Ключ | Смысл |
|------|-------|
| `--check` | проверить ТЗ и напечатать отчёт |
| `--check-only` | только проверка, без сборки |
| `--strict` | код выхода 1 при ошибках, документ не собирается |
| `--check-pages` | эвристическая проверка полупустых страниц через Microsoft Word (нужны Windows, Word, `pywin32`) |
Полный каталог замечаний — в `checker.md`.
### Таблицы и листинги через границу страницы
| Ключ | По умолчанию | Значения |
|------|--------------|----------|
| `--table-continuation` | `word` | `word` — после сохранения Word COM режет по реальной пагинации и подписывает «Продолжение Таблицы N»; `off` и `soft` — одна таблица без автоподписи; `legacy` и `caption` — резать по оценке высоты (возможны артефакты) |
| `--listing-continuation` | `word` | те же значения для листингов |
| `--table-repeat-header` | выкл. | повторять шапку таблицы на каждом фрагменте после разрыва |
Режим `word` требует Windows, установленный Microsoft Word и пакет `pywin32`. Без Word — `--table-continuation off`.
### Диаграммы
| Ключ | По умолчанию | Смысл |
|------|--------------|-------|
| `--plantuml-jar` | вшитый, иначе `PLANTUML_JAR` | путь к `plantuml.jar` |
| `--kroki-url` | `KROKI_URL`, иначе `http://localhost:8000` | локальный Kroki |
| `--diagram-fallback {local,remote,off}` | `remote` | что делать при недоступности локального рендера: `remote` — обратиться к `kroki.io`; `local` — ошибка; `off` — не рисовать |
| `--diagram-format {png,svg}` | `png` | формат вставки; `svg` даёт вектор с PNG-запасным (Word 2016+) |
| `--diagram-scale` | `2` | масштаб растра PlantUML и локального Mermaid; размер на странице как при 1 |
| `--schemes` | рядом с приложением или с `.md` | путь к `md2gost.schemes.json` |
| `--install-chromium` | — | скачать headless Chromium для Mermaid и выйти |
### Прочее
| Ключ | По умолчанию | Смысл |
|------|--------------|-------|
| `--styles` | пресет типа | путь к `md2gost.styles.json` |
| `--syntax-highlighting` | выкл. | подсветка синтаксиса в листингах |
| `--debug` | выкл. | отладочные данные в документе |
---
## 4. Импорт Word → Markdown
```bash
python -m word2md report.docx -o report.md
```
| Ключ | Смысл |
|------|-------|
| `-o`, `--output` | путь к `.md`, по умолчанию рядом с DOCX |
| `--media-dir` | каталог картинок, по умолчанию `<имя>_media` |
| `--keep-toc-pages` | сохранить строки содержания Word (иначе остаётся только `[TOC]`) |
| `--pagebreaks` | писать `---` на разрывах страниц |
Что распознаётся: заголовки и спецразделы, содержание, подписи «Рисунок N — …» / «Таблица N» / «Листинг N» (включая склейку «Продолжение…»), таблицы с объединением в `^` и `>`, листинги, формулы OMML в `$$…$$`, библиография `[n]: …`, ссылки `[n]`, часть перекрёстных ссылок восстанавливается как `@Рисунок:fig1_1`.
Что теряется: исходники PlantUML и Mermaid (остаются только картинки), язык оградки листингов, осмысленные `id` меток, надписи и SmartArt, сноски, плавающие рисунки. Титул и бланк задания до первого заголовка пропускаются, колонтитулы не импортируются.
После импорта результат имеет смысл довести промптом `ai/prompts/from-word.md` и проверить `--check`.
---
## 5. Экспорт в FODT
```bash
python -m md2fodt report.md -o report.fodt
```
Ключ `--emdash-to-hyphen` работает так же. PDF через LaTeX — каталог `md2latex/`.
---
## 6. Конфигурационные файлы
| Файл | Где лежит | Назначение |
|------|-----------|------------|
| `md2gost.user.json` | рядом с приложением | ФИО, группа, блок `metadata` со свойствами DOCX |
| `md2gost.schemes.json` | рядом с приложением, рядом с `.md`, либо путь в `--schemes` | схемы диаграмм: `title`, `docs`, `ai-prompt`, `includes`, `prefix`, `postfix`, `theme` |
| `md2gost.styles.json` | рядом с `.md` или путь в `--styles` | оверлей оформления поверх пресета |
| `info_conv.yaml` | рядом с `.md` | институт, кафедра, дисциплина, преподаватель, должность |
| `.md2gost-cache/` | рядом с `.md` | кэш отрисованных диаграмм |
| `md2gost.include-cache.json`, `include-cache/` | рядом с приложением | кэш скачанных `.puml` |
Приоритет загрузки, где каждый следующий слой сильнее: встроенный шаблон → файл рядом с приложением → файл рядом с `.md` → явный путь в ключе.
### `info_conv.yaml`
```yaml
Институт: Институт информационных технологий (ИИТ)
Кафедра: Кафедра цифровой трансформации (ЦТ)
Дисциплина: Проектирование архитектуры цифровой организации
Преподаватель: Иванов Иван Иванович
Должность: старший преподаватель, кафедра ЦТ
```
Плоский список `ключ: значение` в UTF-8. Строки с `#` — комментарии. ФИО и группа студента из файла **не читаются**: они берутся из профиля или ключей `--student` / `--group`.
### Автотитул: условия срабатывания
1. Включён `--auto-title` (или галочка «Автотитул»).
2. Не задан готовый `--title`.
3. Есть `info_conv.yaml` рядом с `.md` **или** блок ` ```title ` в начале файла.
4. Известны ФИО и группа — иначе конвертация останавливается с подсказкой «Для генерации титула нужны ФИО и группа студента».
Номер работы и год берутся из блока ` ```title ` (`number`, `year`); значения из блока сильнее `info_conv.yaml`. По умолчанию номер `1`, год — текущий.
### `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 }
}
}
```
Допустимые имена стилей: `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`, `bold`, `italic`, `all_caps`, `underline`, `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`.
Неизвестный ключ или неизвестное имя стиля — ошибка запуска. Не настраивается через JSON: табуляция оглавления, отступы списков, оформление формул, цвет текста (всегда чёрный).
---
## 7. Переменные окружения
| Переменная | Назначение |
|------------|------------|
| `PLANTUML_JAR` | путь к `plantuml.jar` |
| `KROKI_URL` | адрес локального Kroki, по умолчанию `http://localhost:8000` |
| `PLAYWRIGHT_BROWSERS_PATH` | каталог Chromium для Mermaid |
| `WORKING_DIR` | родительский каталог кэша диаграмм |
---
## 8. Где что в окне
| Меню | Содержимое |
|------|------------|
| Основные | тип документа, выходной путь, нумерация заголовков, содержание, режимы продолжения таблиц и листингов, смещение страниц |
| Настройки → Файлы | шаблон, титульный лист, бланк задания, стили JSON |
| Настройки → Студент | ФИО и группа (спрашиваются при первом запуске) |
| Настройки → Метаданные | свойства DOCX |
| Настройки → Диаграммы | PlantUML, Graphviz для DFD, Chromium для Mermaid, Kroki, формат, сброс кэша |
| Шаблоны UML | редактор `md2gost.schemes.json` |
| Справка | инструкция, документация, схемы, промпт для ИИ |
Кнопка «Импорт DOCX→MD» и перетаскивание `.docx` запускают импорт; перетаскивание `.md` подставляет файл для сборки.
---
## 9. Установка
```bash
pip install -e .
pip install pywin32 # Windows: продолжение таблиц через Word COM
```
Нужен Python 3.10 или новее (для DFD — 3.11 и новее). Для PlantUML нужна Java; для Mermaid — Chrome, Edge или скачиваемый Chromium; для DFD — Graphviz. IDEF0 рисуется локально без внешних зависимостей.
+567
View File
@@ -0,0 +1,567 @@
# Диаграммы в md2gost — справочник нотаций
Конвертер сам отрисовывает исходники диаграмм и вставляет их как Рисунок. Экспортировать схему в PNG вручную не нужно и вредно: картинка не пересобирается при правке.
---
## 1. Общие правила
```markdown
Диаграмма прецедентов приведена на @Рисунок:uc_main.
%uc_main Диаграмма прецедентов
```
…и сразу после — fenced-блок с исходником.
| Элемент | Правило |
|---------|---------|
| Метка и подпись | строка `%id Подпись` **перед** блоком |
| Ссылка в тексте | `@Рисунок:id` в предложении **до** блока |
| `id` | латиница, цифры, `_`; начинается с буквы; без дефисов |
| `+listing` в подписи | дополнительно выводит исходник схемы как Листинг со своим номером |
| `+landscape` в подписи | размещает рисунок на альбомной странице |
Подпись не должна содержать самостоятельных слов `listing` и `landscape` — они будут вырезаны как флаги.
Внутри блоков диаграмм типографская автозамена **не** работает, поэтому двойные кавычки и дефисы в синтаксисе схем сохраняются как есть. Это единственные защищённые блоки: в блоках кода кавычки заменяются.
---
## 2. Таблица оградок
| Оградка | Тело | Движок |
|---------|------|--------|
| ` ```uml `, ` ```plantuml ` | полный PlantUML с `@startuml` … `@enduml` | PlantUML |
| ` ```uml-c4 `, ` ```c4 ` | макросы C4 Container, без обёртки | PlantUML + stdlib C4 |
| ` ```uml-c4context ` | макросы C4 System Context | PlantUML + stdlib C4 |
| ` ```uml-c4component ` | макросы C4 Component | PlantUML + stdlib C4 |
| ` ```usecase `, ` ```uml-usecase ` | `actor`, `usecase`, связи, без обёртки | PlantUML |
| ` ```archimate `, ` ```uml-archimate ` | макросы ArchiMate 3.2, без обёртки | PlantUML + stdlib ArchiMate |
| ` ```mermaid `, ` ```mmd ` | синтаксис Mermaid | локальный Mermaid или Kroki |
| ` ```idef0 `, ` ```uml-idef0 ` | DSL IDEF0 | локальный рендер (Pillow) |
| ` ```dfd `, ` ```uml-dfd `, ` ```data-flow-diagram `, ` ```yourdon ` | DSL потоков данных | Graphviz или Kroki |
Правило про обёртку: в ` ```uml ` пишешь `@startuml` … `@enduml` **обязательно**; в схемах (`c4`, `usecase`, `archimate` и любых из `md2gost.schemes.json`) — **никогда**, схема подставит обёртку и `!include` сама.
---
## 3. PlantUML: готовые каркасы
### Диаграмма прецедентов
~~~markdown
%uc_main Диаграмма прецедентов системы
```uml
@startuml
left to right direction
skinparam packageStyle rectangle
actor "Студент" as student
actor "Преподаватель" as teacher
actor "Администратор" as admin
rectangle "Портал заявок" {
usecase "Подать заявку" as UC1
usecase "Проверить статус" as UC2
usecase "Согласовать заявку" as UC3
usecase "Аутентифицироваться" as UC4
usecase "Управлять справочниками" as UC5
}
student --> UC1
student --> UC2
teacher --> UC3
admin --> UC5
UC1 ..> UC4 : <<include>>
UC3 ..> UC4 : <<include>>
@enduml
```
~~~
### Диаграмма классов
~~~markdown
%class_domain Диаграмма классов предметной области
```uml
@startuml
skinparam classAttributeIconSize 0
class Request {
- id: int
- createdAt: datetime
- status: Status
+ submit(): void
+ cancel(): void
}
class Client {
- id: int
- name: str
- inn: str
}
enum Status {
NEW
IN_PROGRESS
DONE
}
class RequestService {
+ create(client: Client): Request
+ close(request: Request): void
}
Client "1" *-- "0..*" Request
Request --> Status
RequestService ..> Request
@enduml
```
~~~
### Диаграмма последовательности
~~~markdown
%seq_submit Последовательность обработки заявки
```uml
@startuml
autonumber
actor Студент as s
participant "Веб-клиент" as web
participant "Сервис заявок" as api
database "PostgreSQL" as db
s -> web : заполняет форму
web -> api : POST /requests
api -> db : INSERT request
db --> api : id
api --> web : 201 Created
web --> s : номер заявки
@enduml
```
~~~
### Диаграмма деятельности
~~~markdown
%act_flow Алгоритм обработки заявки
```uml
@startuml
start
:Принять заявку;
if (Данные полные?) then (да)
:Зарегистрировать;
:Назначить исполнителя;
else (нет)
:Вернуть на доработку;
stop
endif
:Исполнить заявку;
:Уведомить клиента;
stop
@enduml
```
~~~
### Диаграмма состояний
~~~markdown
%state_req Состояния заявки
```uml
@startuml
[*] --> Новая
Новая --> ВРаботе : назначен исполнитель
ВРаботе --> Исполнена : работы завершены
ВРаботе --> Отклонена : отказ
Исполнена --> [*]
Отклонена --> [*]
@enduml
```
~~~
### ER-диаграмма
~~~markdown
%erd_main Логическая модель данных
```uml
@startuml
hide circle
skinparam linetype ortho
entity "Клиент" as client {
* id : bigint <<PK>>
--
* name : varchar(255)
inn : varchar(12)
}
entity "Заявка" as request {
* id : bigint <<PK>>
--
* client_id : bigint <<FK>>
* created_at : timestamp
* status : varchar(32)
}
entity "Позиция заявки" as item {
* id : bigint <<PK>>
--
* request_id : bigint <<FK>>
* product : varchar(255)
* qty : int
}
client ||--o{ request
request ||--o{ item
@enduml
```
~~~
### Диаграмма компонентов и развёртывания
~~~markdown
%deploy_prod Схема развёртывания
```uml
@startuml
node "Сервер приложений" {
component "API заявок" as api
component "Планировщик" as sched
}
node "Сервер БД" {
database "PostgreSQL" as db
}
node "Рабочее место" {
component "Браузер" as browser
}
browser --> api : HTTPS
api --> db : SQL
sched --> db : SQL
@enduml
```
~~~
---
## 4. C4
Три уровня — три оградки. В теле только макросы; обёртка и `!include` добавляются схемой.
### Уровень 1: контекст
~~~markdown
%c4ctx Диаграмма контекста системы
```uml-c4context
Person(student, "Студент", "Подаёт заявки и отслеживает статус")
Person(staff, "Сотрудник деканата", "Обрабатывает заявки")
System(portal, "Портал заявок", "Приём и обработка заявок студентов")
System_Ext(sso, "Корпоративный SSO", "Единая аутентификация")
System_Ext(mail, "Почтовый сервис", "Уведомления")
Rel(student, portal, "Подаёт заявку", "HTTPS")
Rel(staff, portal, "Обрабатывает заявки", "HTTPS")
Rel(portal, sso, "Аутентифицирует пользователей", "OIDC")
Rel(portal, mail, "Отправляет уведомления", "SMTP")
```
~~~
### Уровень 2: контейнеры
~~~markdown
%c4cont Диаграмма контейнеров +landscape
```uml-c4
Person(student, "Студент", "Подаёт заявки")
System_Boundary(portal, "Портал заявок") {
Container(spa, "Веб-приложение", "React", "Интерфейс пользователя")
Container(api, "Сервис заявок", "FastAPI", "Бизнес-логика и валидация")
Container(worker, "Обработчик очереди", "Python", "Асинхронные задачи")
ContainerDb(db, "Основная БД", "PostgreSQL", "Заявки, клиенты, история")
ContainerDb(queue, "Очередь", "RabbitMQ", "Задачи на обработку")
}
System_Ext(sso, "Корпоративный SSO", "Аутентификация")
Rel(student, spa, "Работает с заявками", "HTTPS")
Rel(spa, api, "Вызывает методы", "REST/JSON")
Rel(api, db, "Читает и пишет", "SQL")
Rel(api, queue, "Публикует задачи", "AMQP")
Rel(worker, queue, "Читает задачи", "AMQP")
Rel(worker, db, "Обновляет статусы", "SQL")
Rel(spa, sso, "Аутентифицируется", "OIDC")
```
~~~
### Уровень 3: компоненты
~~~markdown
%c4comp Компоненты сервиса заявок
```uml-c4component
Container_Boundary(api, "Сервис заявок") {
Component(ctrl, "Контроллер заявок", "FastAPI Router", "Обработка HTTP-запросов")
Component(svc, "Служба заявок", "Python", "Правила обработки и переходы статусов")
Component(repo, "Репозиторий заявок", "SQLAlchemy", "Доступ к данным")
Component(auth, "Модуль авторизации", "Python", "Проверка прав по роли")
}
ContainerDb(db, "Основная БД", "PostgreSQL", "Заявки и клиенты")
Rel(ctrl, auth, "Проверяет права")
Rel(ctrl, svc, "Вызывает")
Rel(svc, repo, "Читает и пишет")
Rel(repo, db, "SQL-запросы")
```
~~~
### Основные макросы C4
| Макрос | Назначение |
|--------|------------|
| `Person(alias, "Имя", "Описание")` | пользователь-человек |
| `Person_Ext(...)` | внешний пользователь |
| `System(alias, "Имя", "Описание")` | система целиком |
| `System_Ext(...)` | внешняя система |
| `System_Boundary(alias, "Имя") { … }` | граница системы |
| `Container(alias, "Имя", "Технология", "Описание")` | контейнер |
| `ContainerDb(...)`, `ContainerQueue(...)` | база данных, очередь |
| `Container_Boundary(alias, "Имя") { … }` | граница контейнера |
| `Component(alias, "Имя", "Технология", "Описание")` | компонент |
| `Rel(from, to, "Действие", "Технология")` | связь |
| `Rel_U/D/L/R(...)` | связь с направлением раскладки |
| `LAYOUT_WITH_LEGEND()`, `LAYOUT_TOP_DOWN()`, `LAYOUT_LEFT_RIGHT()` | раскладка |
Правила качества: описания на русском, технологии — как принято (`PostgreSQL`, `React`); один уровень абстракции на диаграмму; 6–15 элементов; связи подписаны действием, а не «использует».
---
## 5. Mermaid
Идёт через локальный рендер (системный Chrome/Edge, Playwright Chromium или QuickJS) либо через Kroki.
~~~markdown
%mmd_flow Схема процесса согласования
```mermaid
flowchart TD
A[Заявка поступила] --> B{Данные полные?}
B -- да --> C[Регистрация]
B -- нет --> D[Возврат на доработку]
C --> E[Назначение исполнителя]
E --> F[Исполнение]
F --> G[Уведомление клиента]
D --> A
```
~~~
Поддерживаются `flowchart`, `sequenceDiagram`, `classDiagram`, `stateDiagram-v2`, `erDiagram`, `gantt`, `journey`, `pie`. Для отчёта по ГОСТ надёжнее PlantUML: Mermaid требует браузера или сети.
---
## 6. IDEF0 (FIPS 183)
Оградка ` ```idef0 `. Тело — не XML и не PlantUML, а собственный DSL. Рендер локальный, Java и сеть не нужны.
### Шапка формы
| Ключ | Значение |
|------|----------|
| `title` | название диаграммы |
| `node` | узел: `A-0`, `A0`, `A1` |
| `number` | номер листа |
| `author`, `project`, `date`, `rev` | поля NIST-формы |
| `status` | `WORKING`, `DRAFT`, `RECOMMENDED`, `PUBLICATION` |
| `context` | `TOP` для контекстной A-0, иначе родительский узел |
| `purpose`, `viewpoint` | цель и точка зрения модели |
| `used_at`, `reader`, `reader_date`, `notes`, `page` | необязательные поля формы |
| `form` | `kit` (по умолчанию, с рамкой) или `plain` |
### Боксы и стрелки
| Запись | Смысл |
|--------|-------|
| `[A0] Имя функции` или `box A0 Имя` | бокс функции |
| `<- Название` или `in` | вход (слева) |
| `^ Название` или `control` | управление (сверху) |
| `-> Название` или `out` | выход (справа) |
| `v Название` или `mech` | механизм (снизу) |
| `A1 <- Заявка [I1]` | явное указание бокса и код ICOM |
| `A1 -> A2 : план закупок` | внутренняя связь между боксами |
| `A1 -> A2.C : график` | связь в конкретный порт (`.I`, `.C`, `.O`, `.M`) |
| `A1, A2, A3 ^ Регламент [C1]` | одна стрелка в несколько боксов |
| `(in Секрет` | туннель на границе листа |
| `in) Локальный` | туннель у бокса |
| `call ИмяМодели` | вызов другой модели |
Стрелки без явного бокса относятся к последнему объявленному боксу.
### Контекстная диаграмма A-0
~~~markdown
%idef_a0 Контекстная диаграмма A-0
```idef0
title Распорядиться товаром
node A-0
number 1
author Иванов И. И.
project Модель торгового предприятия
date 08.02.2026
status PUBLICATION
context TOP
[A0] Распорядиться товаром
<- Спрос
^ Нормативная документация
-> Товар
v Персонал
```
~~~
### Декомпозиция A0
~~~markdown
%idef_a0d Декомпозиция A0
```idef0
title Распорядиться товаром
node A0
number 2
author Иванов И. И.
project Модель торгового предприятия
context A-0
purpose Обеспечить наличие товара и его отпуск покупателю
viewpoint Руководитель торгового предприятия
[A1] Принять и оценить спрос
[A2] Закупить и хранить
[A3] Отгрузить товар
A1 <- Спрос [I1]
A2 <- Предложения поставщиков [I2]
A1, A2, A3 ^ Нормативная документация [C1]
A1 ^ Ассортиментная политика [C2]
A3 ^ Условия поставки [C3]
A1 -> A2 : план закупок
A1 -> A2.C : график поставок
A2 -> A3 : товар к отгрузке
A3 -> Товар [O1]
A3 -> Акт отгрузки [O2]
A1, A2, A3 v Персонал [M1]
A2 v Склад [M2]
```
~~~
Правила нотации: контекстная диаграмма — ровно один бокс; в декомпозиции 3–6 боксов «лесенкой» слева-сверху вниз-вправо; подписи стрелок — существительные; управление приходит сверху, механизм снизу.
---
## 7. DFD (диаграммы потоков данных)
Оградка ` ```dfd `. Синтаксис пакета `data-flow-diagram`. Рендер через Graphviz (вшитый, из `PATH` или скачанный) либо Kroki.
### Узлы и потоки
| Ключевое слово | Смысл |
|----------------|-------|
| `process Имя Подпись` | процесс |
| `entity Имя Подпись` | внешняя сущность |
| `store Имя Подпись` | хранилище данных |
| `control Имя Подпись` | элемент управления |
| `channel Имя Подпись` | канал |
| `style context` | оформление контекстной диаграммы |
| Поток | Смысл |
|-------|-------|
| `A --> B подпись` | поток данных |
| `A ->> B подпись` | двойной поток |
| `A <-> B подпись` | двунаправленный |
| `A ::> B подпись` | сигнал |
### Контекстная диаграмма
~~~markdown
%dfd_ctx Контекстная диаграмма потоков данных
```dfd
style context
entity Client Клиент
entity Warehouse Склад
process System Система учёта заявок
Client --> System заявка
System --> Client статус заявки
System --> Warehouse накладная
```
~~~
### Диаграмма нулевого уровня
~~~markdown
%dfd_l0 Диаграмма потоков данных нулевого уровня
```dfd
entity Client Клиент
entity Warehouse Склад
process Accept Принять заявку
process Register Зарегистрировать заявку
process Ship Сформировать накладную
store Orders Заявки
store Log Журнал операций
Client --> Accept заявка
Accept --> Orders новая заявка
Orders --> Register данные заявки
Register --> Log запись об изменении
Register --> Ship подтверждённая заявка
Ship --> Warehouse накладная
Ship --> Client уведомление
```
~~~
Правила нотации: на контекстной диаграмме один процесс; хранилища не соединяются напрямую с внешними сущностями; каждый процесс имеет и вход, и выход.
---
## 8. Что нужно для отрисовки
| Нотация | Требования | Запасной вариант |
|---------|------------|------------------|
| PlantUML, C4, ArchiMate, Use Case | Java и `plantuml.jar` (вшит в exe-сборку) | локальный Kroki, затем `kroki.io` при `--diagram-fallback remote` |
| Mermaid | системный Chrome или Edge; либо скачанный Chromium; либо QuickJS | Kroki |
| IDEF0 | ничего, локальный рендер | не требуется |
| DFD | Graphviz (вшитый, из `PATH` или кнопка «Скачать Graphviz») | Kroki |
Ключи: `--plantuml-jar`, `--kroki-url`, `--diagram-fallback local|remote|off`, `--diagram-format png|svg`, `--diagram-scale N` (по умолчанию 2 — качество растра).
Кэш готовых картинок — каталог `.md2gost-cache` рядом с `.md`; при правке исходника схемы картинка пересобирается автоматически.
---
## 9. Свои схемы
Файл `md2gost.schemes.json` (рядом с приложением, рядом с `.md` или путь в `--schemes`) задаёт новые оградки:
```json
{
"bpmn_like": {
"title": "Процессная схема",
"docs": "Шпаргалка макросов для подсказки в интерфейсе",
"ai-prompt": "Фрагмент, который дописывается в промпт для ИИ",
"includes": ["https://example.org/macros.puml"],
"prefix": "@startuml\n",
"postfix": "\n@enduml",
"theme": ""
}
}
```
`id` схемы: строчная латиница, цифры, `_`, начинается с буквы. После добавления доступны оградки ` ```bpmn_like ` и ` ```uml-bpmn_like `; в теле пишутся только макросы.
Слои загрузки, где каждый следующий сильнее: встроенный шаблон → файл рядом с приложением → файл рядом с `.md` → `--schemes`.
+459
View File
@@ -0,0 +1,459 @@
# Диалект Markdown md2gost — полная спецификация
Справочник синтаксиса конвертера md2gost (Markdown → DOCX по ГОСТ 7.32-2017 и ТЗ РТУ МИРЭА). Всё, что здесь описано, извлечено из кода парсера, нумератора и проверки, а не из пользовательских инструкций.
Базовый парсер — Marko с расширением GFM. Всё, что не перечислено ниже как расширение, работает как обычный GitHub Flavored Markdown.
---
## 1. Расширения относительно обычного Markdown
| Конструкция | Смысл |
|-------------|-------|
| `# *ЗАГОЛОВОК` | раздел без автоматической нумерации |
| `[TOC]` | точка сборки содержания |
| `%id Подпись` | метка и подпись следующего объекта |
| `![alt](path "%id Подпись")` | метка и подпись рисунка-файла |
| `@Тип:id` | перекрёстная ссылка с автоподстановкой номера |
| `[1]: описание` | запись списка источников |
| `[1]`, `[2, 3]`, `[2, c. 21-25]` | ссылка на источник в тексте |
| `^` / `>` в ячейке таблицы | объединение ячеек |
| ` ```uml `, ` ```mermaid `, ` ```idef0 `, ` ```dfd `, ` ```uml-<схема>` | диаграмма, отрисовываемая конвертером |
| ` ```title ` | данные для автогенерации титульного листа |
| `+listing`, `+landscape` в строке `%` | флаги оформления объекта |
---
## 2. Заголовки
Синтаксис: от нуля до трёх ведущих пробелов, затем `#`…`######`, пробел, текст.
Регулярное выражение парсера: `^ {0,3}(#{1,6})((?=\s)[^\n]*?|[^\n\S]*)(?:(?<=\s)(?<!\\)#+)?[^\n\S]*$`
**Звёздочка** первым непробельным символом текста заголовка делает раздел ненумерованным и удаляется из видимого текста:
```markdown
# *ВВЕДЕНИЕ → раздел без номера
# 1 Анализ → нумерованный раздел
```
Пробел после `#` обязателен для корректной работы проверки (`^(#{1,6})\s+(\*?)(.+)$`).
### Спецразделы
Распознаются по тексту заголовка, приведённому к прописным. Список из кода проверки:
`СОДЕРЖАНИЕ`, `ВВЕДЕНИЕ`, `ЗАКЛЮЧЕНИЕ`, `СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ`, `СПИСОК ИСПОЛЬЗУЕМЫХ ИСТОЧНИКОВ`, `ПРИЛОЖЕНИЕ`, `ПРИЛОЖЕНИЯ`, а также любой заголовок, начинающийся на `ПРИЛОЖЕН`.
Для H1 из этого набора звёздочка обязательна — иначе ошибка `heading.numbered_special`.
Оформление в DOCX:
| Заголовок H1 | Выравнивание |
|--------------|--------------|
| `СОДЕРЖАНИЕ`, `СПИСОК ИСПОЛЬЗОВАННЫХ/ИСПОЛЬЗУЕМЫХ ИСТОЧНИКОВ` | по центру |
| `ВВЕДЕНИЕ`, `ЗАКЛЮЧЕНИЕ`, `ПРИЛОЖЕНИЯ`, нумерованные разделы | слева с абзацным отступом |
`СОДЕРЖАНИЕ` исключается из собираемого оглавления.
### Нумерация заголовков
| Режим CLI | Поведение |
|-----------|-----------|
| `--heading-numbering manual` (по умолчанию) | цифры остаются из markdown: `# 1 …`, `## 1.1 …`; автонумерация Word отключена |
| `--heading-numbering auto` | ведущие цифры (`^\d+(?:\.\d+)*\.?\s+`) снимаются с текста заголовка, номера ставит многоуровневый список Word |
При `manual` номера пиши сам. При `auto` их можно не писать — но безопаснее всегда писать и запускать конвертер в `manual`.
### `[TOC]`
Блочный элемент, шаблон `\[(TOC)\]+`. **Только прописными** — `[toc]` не распарсится (хотя проверка его увидит и замечания не выдаст). Ставится отдельной строкой, обычно сразу под `# *СОДЕРЖАНИЕ`.
Режимы сборки: `--toc native` (по умолчанию, поле Word, обновляется при открытии документа) и `--toc manual` (md2gost сам считает страницы).
---
## 3. Метки объектов: строка `%`
Шаблон: `\%(\w+)( (.+))?` — блочный элемент, прерывает открытый абзац.
- `id` = `\w+`: буквы, цифры, `_`. **Дефис, точка, пробел внутри `id` недопустимы.** Практическое правило: `[A-Za-z][A-Za-z0-9_]*`.
- Подпись необязательна: `%eq1` — валидная строка.
- Метка применяется к **следующему непустому блоку**. Пустые строки между меткой и объектом допустимы.
- Если подряд идут две метки, к объекту привяжется вторая.
- `%` в середине абзаца — обычный текст, не метка.
### Флаги в строке подписи
Шаблоны: `(?i)(?:^|\s)\+?listing\b` и `(?i)(?:^|\s)\+?landscape\b`.
| Флаг | Действие |
|------|----------|
| `+listing` | у диаграммы дополнительно выводится Листинг с её исходником (получает свой номер) |
| `+landscape` | объект размещается на альбомной странице |
Важные следствия из регулярных выражений:
1. Плюс необязателен: `listing` без плюса тоже сработает как флаг.
2. Регистр не важен: `+Landscape` = `+landscape`.
3. Флаги вырезаются из текста подписи в любом месте строки. **Поэтому подпись не должна содержать самостоятельных слов `listing` и `landscape`** — они исчезнут из подписи, а объект получит нежелательное оформление.
### Метка рисунка-файла
Метка задаётся в title изображения:
```markdown
![Архитектура системы](images/arch.png "%fig_arch Архитектура информационной системы")
```
Тот же шаблон `%(\w+)( (.+))?` и те же флаги.
### Куда попадает подпись
| Объект | Положение подписи в DOCX |
|--------|--------------------------|
| Рисунок и диаграмма | под объектом |
| Таблица | надпись над таблицей |
| Листинг | надпись над листингом |
| Формула | номер в скобках справа от формулы |
---
## 4. Перекрёстные ссылки `@Тип:id`
Шаблон: `@(\w+):(\w+)`. Пробелы вокруг двоеточия недопустимы. Знаки препинания после ссылки не съедаются, поэтому `@Рисунок:arch.` и `@Таблица:cmp,` работают корректно.
Принимаемые типы (регистр не важен при разрешении ссылки):
| Тип | Синонимы |
|-----|----------|
| `Рисунок` | `figure` |
| `Таблица` | `table` |
| `Листинг` | `listing` |
| `Формула` | `formula`, `equation` |
В документе ссылка разворачивается в «Тип Номер», например `Рисунок 1.1`. Неизвестная метка даёт в тексте `Рисунок?` — визуальный маркер ошибки; проверка сообщает `ref.dangling`.
Каноническая форма — русские названия: `@Рисунок:id`, `@Таблица:id`, `@Листинг:id`, `@Формула:id`.
### Соответствие меток и ссылок
Проверка сверяет два множества:
- метки — из `^%(\w+)` и из title картинок;
- ссылки — из `@[\wА-Яа-я]+:(\w+)`.
Метка без ссылки → предупреждение `ref.unused`. Ссылка без метки → ошибка `ref.dangling`. Правило простое: **сколько меток, столько и адресатов ссылок**.
---
## 5. Нумерация объектов
Области нумерации задаются профилем `--type`:
| Область | Профили | Вид номера |
|---------|---------|------------|
| `section` | `practice`, `coursework`, `vkr`, `APID_coursework` | `1.1`, `2.3`; в приложении — `Б.1` |
| `continuous` | `PIS_custom` | `1`, `2`, `3` на весь документ |
Счётчик раздела увеличивается только на **нумерованных H1** (то есть на `# 1 …`, а не на `# *ВВЕДЕНИЕ`).
Номер получают все объекты **кроме формул**: формуле номер присваивается, только если в исходном тексте есть `@Формула:id` (или `@Formula:id`) с её меткой. Формула без ссылки остаётся без номера.
Диаграмма с флагом `+listing` получает два номера под одной меткой: номер Рисунка и номер Листинга. Ссылки `@Рисунок:id` и `@Листинг:id` разрешатся каждая в свой номер.
---
## 6. Таблицы
Синтаксис GFM: строка заголовков, строка-разделитель, строки данных. Выравнивание через `:---`, `:---:`, `---:` поддерживается. Экранированный `\|` не считается границей столбца.
### Объединение ячеек
| Маркер | Смысл |
|--------|-------|
| `^` или `^^` | ячейка — продолжение ячейки **сверху** (rowspan) |
| `>` или `>>` | ячейка — продолжение ячейки **слева** (colspan) |
Маркер должен быть **единственным** содержимым ячейки после обрезки пробелов. Ограничения проверки: маркеры запрещены в строке заголовков (`table.merge_header`), `>` запрещён в первом столбце (`table.merge_col`).
```markdown
%tab_req Требования к системе
| Категория | Требование |
|-----------|------------|
| Производительность | обработка 1000 заявок в час |
| ^ | время отклика не более 2 с |
| Надёжность | доступность 99,5 % |
```
### Правила ТЗ
- Графа «№ п/п» запрещена (`table.nopp`, шаблон `№\s*п\s*/\s*п`).
- Таблицу из одной строки данных не оформляют таблицей.
- Таблицы и рисунки не размещают внутри списков.
### Продолжение на следующей странице
DOCX не умеет сам подписывать «Продолжение Таблицы N». Режимы `--table-continuation` и `--listing-continuation`: `word` (по умолчанию; после сохранения Word COM режет по реальной пагинации), `off`, `soft`, `legacy`, `caption`. Режим `word` требует Windows, Microsoft Word и пакет `pywin32`.
---
## 7. Листинги
Любой fenced-блок с языком, который **не** является языком диаграммы, становится Листингом.
~~~markdown
%code_volume Процедура расчёта объёма
```python
def volume(count: int, size: float) -> float:
return count * size
```
~~~
Подсветка синтаксиса включается ключом `--syntax-highlighting` (по умолчанию выключена).
### Ловушка: автозамена внутри кода
Препроцессор применяет типографские правки ко всему тексту, **исключая только блоки диаграмм**. Блоки кода не защищены:
| В исходнике | В DOCX |
|-------------|--------|
| `print("hello")` | `print(«hello»)` |
| `x = a - b` | `x = a — b` |
Обход: одинарные кавычки в примерах кода и вычитание без пробелов (`a-b`). Ключ `--emdash-to-hyphen` меняет направление замены тире, но кавычки правит всегда.
---
## 8. Формулы
| Форма | Шаблон | Назначение |
|-------|--------|------------|
| Выключная | `\$\$([\S\s]*?)\$\$` | отдельный блок, нумеруемая формула |
| Инлайновая | `\$(.*?)\$` | обозначения в тексте: `$N$` |
```markdown
%eq_volume
$$
V = N \times S
$$
где $N$ — число записей; $S$ — средний размер записи, байт.
Оценка по (@Формула:eq_volume) используется при выборе хранилища.
```
Номер присваивается только при наличии ссылки. Расшифровка обозначений начинается со слова «где» без двоеточия.
---
## 9. Диаграммы
Fenced-блок с языком диаграммы превращается в Рисунок; исходник отрисовывается конвертером.
### Встроенные языки
| Оградка | Движок |
|---------|--------|
| `uml`, `plantuml` | PlantUML (java + `plantuml.jar`, иначе Kroki) |
| `mermaid`, `mmd` | локальный Mermaid (браузер / QuickJS), иначе Kroki |
| `idef0`, `uml-idef0` | локальный рендер на Pillow, без Java и сети |
| `dfd`, `uml-dfd`, `data-flow-diagram`, `yourdon`, `uml-yourdon` | пакет `data-flow-diagram` + Graphviz, иначе Kroki |
### Схемы из `md2gost.schemes.json`
Оградка `uml-<id>` или просто `<id>`, где `<id>` — идентификатор схемы (`^[a-z][a-z0-9_]*$`). Встроенные схемы:
| id | Что это | Оградка |
|----|---------|---------|
| `c4` | C4 Container | ` ```uml-c4 ` / ` ```c4 ` |
| `c4context` | C4 System Context | ` ```uml-c4context ` |
| `c4component` | C4 Component | ` ```uml-c4component ` |
| `usecase` | UML Use Case | ` ```uml-usecase ` / ` ```usecase ` |
| `archimate` | ArchiMate 3.2 | ` ```uml-archimate ` / ` ```archimate ` |
В теле схемы пишутся **только макросы**: обёртку `@startuml` … `@enduml` и `!include` подставляет схема. В блоке ` ```uml ` наоборот — `@startuml` … `@enduml` обязательны.
Неизвестный `uml-<id>` даёт ошибку рендера: `Схема диаграммы «<id>» не найдена`.
Подробный синтаксис всех нотаций — в `diagrams.md`, ArchiMate — в `archimate.md`.
---
## 10. Источники
### Запись списка
Шаблон строки: `^\[(\d+(?:\.\d+)?)\]:\s*(.+)$`
Записи размещаются под заголовком, совпадающим с `^#\s*\*?\s*СПИСОК\s+ИСПОЛЬЗОВАНН?ЫХ\s+ИСТОЧНИКОВ\s*$` — то есть «СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ» или «СПИСОК ИСПОЛЬЗУЕМЫХ ИСТОЧНИКОВ».
```markdown
# *СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ
[1]: Иванов И. И. Проектирование информационных систем: учебник. — М.: Инфра-М, 2023. — 320 с.
[2]: ГОСТ 7.32-2017. Отчёт о научно-исследовательской работе. — М.: Стандартинформ, 2018. — 32 с.
```
Препроцессор сам расставит пустые строки между записями, но безопаснее писать их сразу.
Для ВКР список делится подразделами `##`; ключ записи получает вид `[1.5]` — номер раздела и номер внутри раздела.
### Ссылка в тексте
Шаблон: `\[(\d+(?:\.\d+)?(?:\s*,\s*\d+(?:\.\d+)?)*)(?:\s*,\s*[cс]\.\s*[\d\-–]+)?\]`
| Форма | Пример |
|-------|--------|
| одиночная | `[1]`, `[1.5]` |
| перечисление | `[2, 3]`, `[2,3]` |
| с указанием страниц | `[2, c. 21-25]`, `[2.18, с. 21–25]` — буква `c` латинская или русская |
В DOCX номер становится внутренней гиперссылкой на запись списка.
Сноски `[^1]` запрещены (`footnote`).
---
## 11. Приложения
```markdown
# *ПРИЛОЖЕНИЯ
Приложение А — Листинг модуля обработки заявок
Приложение Б — Схема базы данных
## Приложение А Листинг модуля обработки заявок
## Приложение Б Схема базы данных
```
- Заголовок приложения распознаётся шаблоном `^ПРИЛОЖЕНИЕ\s+([А-ЯA-ZЁ])(?:\s+(.+))?$` без учёта регистра. Оформляется как Heading 3 по центру с разрывом страницы; название — отдельной строкой под ним.
- Допустимые буквы: `АБВГДЕЖИКЛМНПРСТУФХЦШЩЭЮЯ`. Запрещённые: `Ё З Й О Ч Ь Ы Ъ` (ошибка `appendix.letter`).
- Если букву не указать или указать запрещённую, конвертер подставит следующую допустимую по порядку.
- При двух и более приложениях после `# *ПРИЛОЖЕНИЯ` нужен перечень обычным текстом, иначе предупреждение `appendix.toc`. Если перечня нет, конвертер вставит его сам, но лучше писать явно.
- Объекты внутри приложения нумеруются с буквой приложения: `Рисунок Б.1`.
---
## 12. Блок ` ```title `
Первый блок с оградкой `title` в файле вырезается до разбора markdown и используется для автогенерации титульного листа (только при `--auto-title`).
````markdown
```title
number: 1
year: 2026
```
# *СОДЕРЖАНИЕ
[TOC]
````
Тело — плоские строки `ключ: значение`. Принимаемые ключи:
| Поле | Ключи |
|------|-------|
| Институт | `institute`, `институт`, `инститиут` |
| Кафедра | `department`, `кафедра` |
| Дисциплина | `discipline`, `дисциплина` |
| Преподаватель | `teacher`, `преподаватель`, `преподователь` |
| Должность преподавателя | `teacher_position`, `должность`, `должность преподавателя` |
| Номер работы | `number`, `номер`, `№`, `n` — по умолчанию `1` |
| Год | `year`, `год` — по умолчанию текущий |
ФИО и группа студента **из блока не читаются**: они берутся из профиля приложения или ключей `--student` / `--group`.
Остальные академические поля можно держать в файле `info_conv.yaml` рядом с `.md`; значения из блока ` ```title ` сильнее.
---
## 13. Автоматические правки текста
Препроцессор применяет ко всему тексту, **кроме блоков диаграмм**:
| Правило | Было | Стало |
|---------|------|-------|
| Русские кавычки | `"текст"` | `«текст»` |
| Тире вместо дефиса между словами | `слово - слово` | `слово — слово` |
| Разделение записей списка источников | склеенные строки `[n]:` | каждая записью-абзацем |
Кавычки не заменяются, если содержимое начинается с `%` (защита строк подписей).
Ключ `--emdash-to-hyphen` включает обратную замену: все `—` становятся `-`.
---
## 14. Прочее
| Конструкция | Поведение |
|-------------|-----------|
| `---`, `***`, `___` | по умолчанию **игнорируются**; с ключом `--hr-pagebreak` дают разрыв страницы |
| Обычные `**жирный**`, `*курсив*`, `` `код` ``, ссылки, списки | работают как в GFM |
| HTML-блоки | не поддерживаются, вставляются как предупреждение в документ |
| Комментарии | синтаксиса комментариев нет |
| Заголовок глубже `###` | парсится, но в оформлении ТЗ не предусмотрен |
| Дефис в обычном тексте | превращается в неразрывный дефис |
---
## 15. Полный минимальный пример
~~~markdown
# *СОДЕРЖАНИЕ
[TOC]
# *ВВЕДЕНИЕ
Цель работы — спроектировать модуль учёта заявок.
# 1 Анализ предметной области
Обобщённая архитектура приведена на @Рисунок:fig_arch.
![Архитектура](images/arch.png "%fig_arch Архитектура информационной системы")
Сравнение подходов сведено в @Таблица:tab_cmp.
%tab_cmp Сравнение подходов к реализации
| Подход | Трудоёмкость | Гибкость |
|--------|--------------|----------|
| Монолит | низкая | низкая |
| Микросервисы | высокая | высокая |
Модульная структура описана в источнике [1].
# 2 Проектирование
Оценка объёма данных выполняется по (@Формула:eq_volume).
%eq_volume
$$
V = N \times S
$$
где $N$ — число записей; $S$ — средний размер записи, байт.
Реализация приведена в @Листинг:code_volume.
%code_volume Процедура расчёта объёма
```python
def volume(count: int, size: float) -> float:
return count * size
```
# *ЗАКЛЮЧЕНИЕ
Спроектирован модуль учёта заявок, оценён объём хранилища.
# *СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ
[1]: Иванов И. И. Проектирование информационных систем: учебник. — М.: Инфра-М, 2023. — 320 с.
~~~
+281
View File
@@ -0,0 +1,281 @@
# *СОДЕРЖАНИЕ
[TOC]
# *ВВЕДЕНИЕ
Обслуживание парка оборудования в производственных подразделениях опирается на заявки, которые до сих пор часто передаются по телефону и фиксируются в электронных таблицах. При таком порядке теряется история обращений, а срок реакции зависит от загруженности диспетчера.
Объектом работы является процесс приёма и исполнения заявок на техническое обслуживание. Предмет работы — архитектура и программная реализация модуля учёта таких заявок.
Цель работы — спроектировать и реализовать модуль учёта заявок, обеспечивающий регистрацию обращений, контроль сроков исполнения и хранение истории обслуживания.
Для достижения цели решаются следующие задачи:
1. Проанализировать процесс обработки заявок и существующие решения для его автоматизации.
2. Сформировать требования к модулю и спроектировать его архитектуру и модель данных.
3. Реализовать ключевые функции модуля и оценить объём хранилища.
В работе применяются структурный анализ процессов, объектно-ориентированное моделирование в нотации UML и сравнительный анализ программных решений.
# 1 Анализ процесса обработки заявок
## 1.1 Описание процесса и его участников
В обработке заявки участвуют три роли: инициатор со стороны производственного подразделения, диспетчер службы обслуживания и исполнитель. Инициатор сообщает о неисправности, диспетчер регистрирует обращение и назначает исполнителя, исполнитель выполняет работы и закрывает заявку.
Границы автоматизируемого процесса и его внешние интерфейсы приведены на @Рисунок:idef_a0.
%idef_a0 Контекстная диаграмма процесса обслуживания
```idef0
title Обработать заявку на обслуживание
node A-0
number 1
project Модуль учёта заявок
context TOP
[A0] Обработать заявку на обслуживание
<- Обращение инициатора
^ Регламент технического обслуживания
-> Закрытая заявка с отметкой о работах
v Служба обслуживания
```
Наблюдение за работой диспетчера в течение недели дало следующие показатели: в среднем 42 обращения в сутки, среднее время регистрации 6 минут, доля обращений с неполными сведениями об оборудовании 18 %. Именно неполнота сведений вызывает повторное обращение к инициатору и удлиняет реакцию на заявку.
## 1.2 Сравнение существующих решений
Для автоматизации процесса рассматривались три варианта: универсальная система управления обслуживанием, модуль в составе используемой на предприятии учётной системы и собственная разработка. Критериями сравнения выбраны стоимость владения, возможность интеграции со справочником оборудования и трудоёмкость внедрения [1].
Результаты сравнения сведены в @Таблица:tab_cmp.
%tab_cmp Сравнение вариантов автоматизации процесса
| Вариант | Стоимость владения | Интеграция со справочником | Трудоёмкость внедрения |
|---------|--------------------|----------------------------|------------------------|
| Универсальная система обслуживания | высокая | через файловый обмен | средняя |
| Модуль учётной системы | средняя | штатная | высокая |
| Собственная разработка | низкая | по REST | низкая |
Универсальная система покрывает больше сценариев, чем требуется подразделению, и вносит внешний файловый обмен, который сам становится источником расхождений [2]. Модуль учётной системы обеспечивает штатную интеграцию, но его доработка требует привлечения поставщика. Собственная разработка выбрана как вариант, закрывающий требования при наименьшей трудоёмкости.
## 1.3 Требования к модулю
Требования сгруппированы по категориям и приведены в @Таблица:tab_req. Формулировки нефункциональных требований опираются на рекомендации по описанию качества программных систем [3].
%tab_req Требования к модулю учёта заявок
| Категория | Требование |
|-----------|------------|
| Функциональные | регистрация заявки с привязкой к единице оборудования |
| ^ | назначение исполнителя и контроль срока |
| ^ | хранение истории обслуживания единицы оборудования |
| Производительность | обработка 100 заявок в час при 20 одновременных пользователях |
| ^ | время отклика интерфейса не более 2 с |
| Надёжность | доступность не ниже 99,5 % в рабочие часы |
| Интеграция | получение справочника оборудования по REST |
Ограничения проекта: развёртывание в контуре предприятия, единая аутентификация через корпоративную службу, хранение данных не менее трёх лет.
# 2 Проектирование модуля
## 2.1 Архитектура модуля
Модуль спроектирован по трёхуровневой схеме: клиентское приложение, сервис прикладной логики и хранилище. Такое разделение позволяет обновлять правила обработки заявок без изменения клиента [4].
Контейнерная структура модуля приведена на @Рисунок:c4_containers.
%c4_containers Контейнеры модуля учёта заявок +landscape
```uml-c4
Person(dispatcher, "Диспетчер", "Регистрирует и распределяет заявки")
Person(worker, "Исполнитель", "Выполняет работы по заявке")
System_Boundary(app, "Модуль учёта заявок") {
Container(spa, "Веб-клиент", "React", "Интерфейс диспетчера и исполнителя")
Container(api, "Сервис заявок", "FastAPI", "Правила обработки и контроль сроков")
ContainerDb(db, "Хранилище заявок", "PostgreSQL", "Заявки, работы, история")
}
System_Ext(equip, "Справочник оборудования", "Учётная система предприятия")
System_Ext(sso, "Корпоративная служба входа", "Единая аутентификация")
Rel(dispatcher, spa, "Регистрирует заявки", "HTTPS")
Rel(worker, spa, "Отмечает выполнение", "HTTPS")
Rel(spa, api, "Вызывает методы", "REST/JSON")
Rel(api, db, "Читает и пишет", "SQL")
Rel(api, equip, "Запрашивает единицы оборудования", "REST")
Rel(spa, sso, "Аутентифицирует пользователя", "OIDC")
```
Справочник оборудования остаётся во внешней учётной системе: дублирование его в модуле привело бы к расхождению данных при выводе оборудования из эксплуатации.
## 2.2 Модель данных
Логическая модель данных охватывает три сущности: заявку, работу по заявке и запись истории обслуживания. Единица оборудования в модуле не хранится, вместо неё сохраняется её идентификатор во внешнем справочнике.
Модель данных приведена на @Рисунок:erd.
%erd Логическая модель данных модуля
```uml
@startuml
hide circle
skinparam linetype ortho
entity "Заявка" as request {
* id : bigint <<PK>>
--
* equipment_ref : varchar(32)
* created_at : timestamp
* due_at : timestamp
* status : varchar(16)
description : text
}
entity "Работа" as work {
* id : bigint <<PK>>
--
* request_id : bigint <<FK>>
* performer : varchar(128)
* started_at : timestamp
finished_at : timestamp
}
entity "Запись истории" as history {
* id : bigint <<PK>>
--
* request_id : bigint <<FK>>
* changed_at : timestamp
* old_status : varchar(16)
* new_status : varchar(16)
}
request ||--o{ work
request ||--o{ history
@enduml
```
Статус заявки хранится строкой из фиксированного набора значений, а не ссылкой на справочник: набор состояний определяется регламентом обслуживания и меняется вместе с версией модуля.
## 2.3 Оценка объёма хранилища
Объём данных за период эксплуатации оценивается произведением числа записей на средний размер записи с учётом коэффициента служебных структур базы данных [5].
%eq_volume
$$
V = N \cdot S \cdot k
$$
где $N$ — число заявок за период; $S$ — средний размер записи вместе со связанными работами и историей, байт; $k$ — коэффициент накладных расходов хранилища.
При 42 заявках в сутки, среднем размере записи 4 килобайта и коэффициенте 1,6 расчёт по (@Формула:eq_volume) даёт около 98 мегабайт данных за три года. Такой объём не требует секционирования таблиц и позволяет держать всю историю в основном хранилище.
# 3 Реализация ключевых функций
## 3.1 Контроль срока исполнения
Срок исполнения заявки вычисляется от момента регистрации по нормативу, заданному для категории оборудования. Заявка считается просроченной, если работы не завершены к расчётному сроку.
Реализация проверки приведена в @Листинг:code_overdue.
%code_overdue Проверка просроченных заявок
```python
from datetime import datetime, timedelta
NORMS_HOURS = {'critical': 4, 'standard': 24, 'planned': 72}
def due_at(created_at: datetime, category: str) -> datetime:
return created_at + timedelta(hours=NORMS_HOURS[category])
def is_overdue(request: dict, now: datetime) -> bool:
if request['status'] == 'closed':
return False
return now > due_at(request['created_at'], request['category'])
```
Нормативы вынесены в отдельную структуру: их изменение регламентом не требует правки логики проверки.
## 3.2 Проверка полноты сведений при регистрации
Доля обращений с неполными сведениями об оборудовании составляла 18 %, поэтому регистрация заявки блокируется до получения идентификатора единицы оборудования из внешнего справочника. Обращение к справочнику выполняется при вводе, а не при сохранении: диспетчер видит ошибку до того, как заполнит остальные поля.
Проверка вернула долю отклонённых при вводе форм на уровне 4 % от общего числа регистраций, остальные обращения были дополнены сведениями сразу.
# *ЗАКЛЮЧЕНИЕ
Проанализирован процесс приёма и исполнения заявок на техническое обслуживание: в нём участвуют три роли, а основной потерей времени является уточнение сведений об оборудовании, затрагивающее почти каждое пятое обращение.
Сформированы требования к модулю и выбран вариант собственной разработки — он закрывает требования подразделения при наименьшей трудоёмкости внедрения и позволяет получать справочник оборудования по REST вместо файлового обмена.
Модуль спроектирован по трёхуровневой схеме; модель данных содержит заявку, работу и запись истории, а справочник оборудования остаётся во внешней системе. Оценка объёма хранилища за три года эксплуатации составила около 98 мегабайт, что не требует секционирования таблиц.
Реализованы контроль срока исполнения с нормативами по категориям оборудования и проверка полноты сведений на этапе ввода: доля неполных обращений снизилась с 18 % до 4 %.
# *СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ
[1]: Ричардс М., Форд Н. Фундаментальный подход к программной архитектуре: паттерны, свойства, проверенные методы. — СПб.: Питер, 2023. — 432 с.
[2]: Клеппман М. Высоконагруженные приложения: программирование, масштабирование, поддержка. — СПб.: Питер, 2022. — 640 с.
[3]: ГОСТ Р ИСО/МЭК 25010-2015. Информационные технологии. Системная и программная инженерия. Требования и оценка качества систем и программного обеспечения. — М.: Стандартинформ, 2022. — 36 с.
[4]: Ньюмен С. От монолита к микросервисам. — М.: ДМК Пресс, 2022. — 274 с.
[5]: Документация PostgreSQL 16. Раздел «Database Physical Storage». — URL: https://www.postgresql.org/docs/16/storage.html (дата обращения: 14.02.2026).
[6]: Кириллина Ю. В. и др. Методические указания по оформлению учебных работ. — М.: РТУ МИРЭА, 2022. — 64 с.
# *ПРИЛОЖЕНИЯ
Приложение А — Схема состояний заявки
Приложение Б — Фрагмент сценария регистрации заявки
## Приложение А Схема состояний заявки
Переходы между состояниями заявки приведены на @Рисунок:state_req. Требования к оформлению схемы приняты по методическим указаниям [6].
%state_req Состояния заявки на обслуживание
```uml
@startuml
[*] --> Новая
Новая --> Назначена : назначен исполнитель
Назначена --> ВРаботе : работы начаты
ВРаботе --> Закрыта : работы завершены
Назначена --> Отклонена : обращение неправомерно
Новая --> Отклонена : обращение неправомерно
Закрыта --> [*]
Отклонена --> [*]
@enduml
```
## Приложение Б Фрагмент сценария регистрации заявки
Последовательность обращений при регистрации заявки приведена на @Рисунок:seq_reg.
%seq_reg Последовательность регистрации заявки
```uml
@startuml
autonumber
actor Диспетчер as d
participant "Веб-клиент" as spa
participant "Сервис заявок" as api
participant "Справочник оборудования" as equip
database "Хранилище" as db
d -> spa : вводит идентификатор оборудования
spa -> api : GET /equipment/{ref}
api -> equip : GET /units/{ref}
equip --> api : сведения об оборудовании
api --> spa : карточка оборудования
d -> spa : заполняет описание неисправности
spa -> api : POST /requests
api -> db : INSERT request
db --> api : идентификатор заявки
api --> spa : номер заявки и срок исполнения
@enduml
```
+224
View File
@@ -0,0 +1,224 @@
# *СОДЕРЖАНИЕ
[TOC]
# Практическая работа №1. Описание предметной области и границ системы
## Цель работы
Описать предметную область складского учёта, выделить участников процесса и зафиксировать границы автоматизируемой системы.
## Ход работы
Рассматривается склад готовой продукции среднего предприятия: три зоны комплектации, один участок приёмки, две смены по четыре кладовщика. Через склад проходит около 400 отгрузок в сутки, приёмка занимает от 20 до 90 минут на партию.
Участники процесса и их задачи приведены в @Таблица:pr1_roles.
%pr1_roles Участники процесса складского учёта
| Участник | Задача | Что получает на выходе |
|----------|--------|------------------------|
| Кладовщик приёмки | сверяет поступившую партию с накладной | акт приёмки |
| Комплектовщик | собирает заказ по заданию | укомплектованный заказ |
| Кладовщик отгрузки | оформляет передачу заказа перевозчику | подписанная накладная |
| Начальник склада | контролирует остатки и сроки | сводка по остаткам |
Границы системы и её внешнее окружение показаны на @Рисунок:pr1_ctx.
%pr1_ctx Контекстная диаграмма потоков данных
```dfd
style context
entity Supplier Поставщик
entity Carrier Перевозчик
entity Sales Отдел продаж
process System Система складского учёта
Supplier --> System товарная накладная
Sales --> System заказ на отгрузку
System --> Carrier накладная на отгрузку
System --> Sales сведения об остатках
```
Границы приняты по участку от приёмки партии до передачи заказа перевозчику. Бухгалтерский учёт и закупки остаются за пределами системы: они ведутся в учётной системе предприятия и получают из системы только сводки.
## Выводы
Основным ограничением процесса является ручной ввод накладных при приёмке: он даёт до 6 % расхождений в учёте остатков и удлиняет приёмку партии в среднем на 25 минут. Именно этот участок определяет требования к системе.
# Практическая работа №2. Модель бизнес-процессов склада
## Цель работы
Построить функциональную модель процесса складского учёта в нотации IDEF0 и определить состав функций верхнего уровня.
## Ход работы
Контекстная диаграмма процесса приведена на @Рисунок:pr2_a0.
%pr2_a0 Контекстная диаграмма A-0
```idef0
title Обеспечить движение товара на складе
node A-0
number 1
project Система складского учёта
context TOP
[A0] Обеспечить движение товара на складе
<- Товарная накладная
<- Заказ на отгрузку
^ Регламент складских операций
-> Подписанная накладная на отгрузку
-> Сводка по остаткам
v Персонал склада
```
Декомпозиция функции верхнего уровня выделила три функции, приведённые на @Рисунок:pr2_a0d.
%pr2_a0d Декомпозиция A0
```idef0
title Обеспечить движение товара на складе
node A0
number 2
project Система складского учёта
context A-0
purpose Сократить расхождения учёта при приёмке и отгрузке
viewpoint Начальник склада
[A1] Принять партию
[A2] Разместить и хранить
[A3] Скомплектовать и отгрузить
A1 <- Товарная накладная [I1]
A3 <- Заказ на отгрузку [I2]
A1, A2, A3 ^ Регламент складских операций [C1]
A2 ^ Схема зонирования склада [C2]
A1 -> A2 : принятая партия
A2 -> A3 : товар к комплектации
A3 -> Подписанная накладная на отгрузку [O1]
A2 -> Сводка по остаткам [O2]
A1, A2, A3 v Персонал склада [M1]
A2 v Складское оборудование [M2]
```
Схема зонирования вынесена в управление, а не во вход: она задаёт правила размещения и не расходуется в процессе.
## Выводы
Функция размещения оказалась единственной, которая одновременно получает управление от схемы зонирования и формирует сводку по остаткам. Расхождения учёта возникают именно на переходе от приёмки к размещению, когда партия физически принята, но ещё не отражена в остатках.
# Практическая работа №3. Модель данных системы
## Цель работы
Спроектировать логическую модель данных системы складского учёта и обосновать состав сущностей.
## Ход работы
Модель данных приведена на @Рисунок:pr3_erd.
%pr3_erd Логическая модель данных системы
```uml
@startuml
hide circle
skinparam linetype ortho
entity "Партия" as batch {
* id : bigint <<PK>>
--
* supplier : varchar(255)
* accepted_at : timestamp
* invoice_no : varchar(32)
}
entity "Товарная позиция" as item {
* id : bigint <<PK>>
--
* batch_id : bigint <<FK>>
* sku : varchar(32)
* qty : numeric(12,3)
zone : varchar(16)
}
entity "Заказ на отгрузку" as order_ {
* id : bigint <<PK>>
--
* created_at : timestamp
* status : varchar(16)
}
entity "Строка заказа" as line {
* id : bigint <<PK>>
--
* order_id : bigint <<FK>>
* sku : varchar(32)
* qty : numeric(12,3)
}
batch ||--o{ item
order_ ||--o{ line
@enduml
```
Остаток по артикулу не хранится отдельным полем, а вычисляется как разность принятого и отгруженного количества. Такое решение исключает расхождение между остатком и первичными документами, но требует индекса по артикулу для приемлемой скорости выборки.
Соответствие сущностей модели функциям процесса приведено в @Таблица:pr3_map.
%pr3_map Соответствие сущностей функциям процесса
| Функция | Сущности | Что изменяется |
|---------|----------|----------------|
| Принять партию | Партия, Товарная позиция | добавляются позиции с нулевой зоной |
| Разместить и хранить | Товарная позиция | заполняется зона размещения |
| Скомплектовать и отгрузить | Заказ на отгрузку, Строка заказа | создаются строки и уменьшается доступное количество |
## Выводы
Модель обходится четырьмя сущностями за счёт вычисляемого остатка. Слабое место решения — скорость расчёта остатка при росте числа позиций: при объёме свыше миллиона строк потребуется материализованное представление.
# Практическая работа №4. Расчёт нагрузки на систему
## Цель работы
Оценить пиковую нагрузку на систему складского учёта и объём хранилища на три года эксплуатации.
## Ход работы
Пиковая интенсивность запросов оценивается по числу операций в смену с учётом коэффициента неравномерности.
%pr4_eq_load
$$
\lambda_{\max} = \frac{Q \cdot m}{T} \cdot k
$$
где $Q$ — число отгрузок в смену; $m$ — среднее число запросов на одну отгрузку; $T$ — длительность смены в секундах; $k$ — коэффициент неравномерности нагрузки.
Расчёт по (@Формула:pr4_eq_load) при 200 отгрузках в смену, 14 запросах на отгрузку, смене 12 часов и коэффициенте 2,5 даёт пиковую интенсивность около 0,16 запроса в секунду.
Реализация расчёта приведена в @Листинг:pr4_code.
%pr4_code Расчёт пиковой интенсивности запросов
```python
def peak_rate(shipments: int, requests_per_shipment: int,
shift_seconds: int, unevenness: float) -> float:
return shipments * requests_per_shipment / shift_seconds * unevenness
```
Результаты расчёта для трёх сценариев роста приведены в @Таблица:pr4_load.
%pr4_load Оценка нагрузки для сценариев роста
| Сценарий | Отгрузок в смену | Пиковая интенсивность, запр./с | Объём за три года, ГБ |
|----------|------------------|-------------------------------|-----------------------|
| Текущий | 200 | 0,16 | 1,4 |
| Рост на 50 % | 300 | 0,24 | 2,1 |
| Двукратный рост | 400 | 0,32 | 2,8 |
## Выводы
Даже при двукратном росте отгрузок пиковая интенсивность остаётся ниже одного запроса в секунду, поэтому одного экземпляра сервиса достаточно. Ограничивающим фактором становится не нагрузка, а расчёт остатков по накопленным первичным документам.
+313
View File
@@ -0,0 +1,313 @@
Ты — генератор документов в диалекте Markdown для конвертера **md2gost** (Markdown → DOCX по ГОСТ 7.32-2017 и методическим указаниям РТУ МИРЭА). Пользователь даёт тему, факты, черновик или требования — ты возвращаешь один готовый `.md`-файл, который конвертер превратит в DOCX без ручной доводки.
# 1. Контракт вывода
1. Ответ = **только текст документа**. Первый символ ответа — первый символ документа.
2. Не оборачивай ответ во внешнюю оградку (` ```markdown `, ` ``` ` и т. п.).
3. Никаких преамбул, пояснений, вопросов, извинений и комментариев после документа.
4. Не выдавай DOCX, HTML, PNG. Не пиши «вставьте сюда картинку» и «здесь должна быть схема».
5. Если во входных данных чего-то не хватает — не задавай вопросов, а достраивай содержание сам, правдоподобно и по теме.
6. Язык документа — русский, научный стиль. Без разговорных оборотов, без «мы с вами», без обращений к читателю.
# 2. Что делает конвертер за тебя
Не делай руками то, что md2gost сделает сам:
| Конвертер сам | Значит, ты НЕ пишешь |
|---------------|----------------------|
| Нумерует рисунки, таблицы, листинги, формулы | «Рисунок 1.1» руками в подписи |
| Подставляет номер в перекрёстную ссылку | «см. рисунок 2 на стр. 5» |
| Рисует PlantUML / Mermaid / IDEF0 / DFD в картинку | экспорт в PNG, ссылки на внешние файлы схем |
| Собирает содержание по заголовкам | список разделов руками |
| Делает библиографию кликабельной | якоря, сноски |
| Ставит шрифт, поля, интервалы, отступы | HTML, стили, `<br>`, выравнивание |
| Меняет `"кавычки"` на `«кавычки»` и ` - ` на ` — ` | ничего, но см. п. 9 про листинги |
# 3. Каркас документа
~~~markdown
# *СОДЕРЖАНИЕ
[TOC]
# *ВВЕДЕНИЕ
Текст введения.
# 1 Название первого раздела
## 1.1 Название подраздела
### 1.1.1 Название пункта
# 2 Название второго раздела
# *ЗАКЛЮЧЕНИЕ
# *СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ
[1]: Иванов И. И. Название книги. — М.: Наука, 2024. — 120 с.
# *ПРИЛОЖЕНИЯ
## Приложение А Название приложения
~~~
Правила заголовков:
- `*` сразу после `#` = раздел **без номера**. Только для спецразделов: СОДЕРЖАНИЕ, ВВЕДЕНИЕ, ЗАКЛЮЧЕНИЕ, СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ, ПРИЛОЖЕНИЯ.
- Спецразделы пишутся **ПРОПИСНЫМИ**.
- Пробел после `#` обязателен: `# *ВВЕДЕНИЕ`, а не `#*ВВЕДЕНИЕ`.
- `[TOC]` — только прописными, отдельной строкой, сразу под `# *СОДЕРЖАНИЕ`.
- Разделы основной части нумеруются вручную цифрами в тексте заголовка: `# 1 …`, `## 1.1 …`, `### 1.1.1 …`.
- Точка в конце заголовка не ставится.
- Заголовок глубже `###` в отчёте не используется.
# 4. Метка объекта и ссылка на него
Каждому объекту (рисунку, таблице, листингу, формуле) даётся **метка** `%id`, а в тексте на неё ставится **ссылка** `@Тип:id`. Номер подставит конвертер.
Правила `id`: латиница, цифры и `_`, начинается с буквы. **Дефис и точка запрещены** (`%fig-1` сломается, нужно `%fig_1`).
| Объект | Где метка | Форма ссылки |
|--------|-----------|--------------|
| Рисунок-файл | в title картинки: `![alt](img.png "%id Подпись")` | `@Рисунок:id` |
| Диаграмма (fenced-блок) | строка `%id Подпись` перед блоком | `@Рисунок:id` |
| Таблица | строка `%id Подпись` перед таблицей | `@Таблица:id` |
| Листинг кода | строка `%id Подпись` перед блоком кода | `@Листинг:id` |
| Формула | строка `%id` перед `$$…$$` (подпись не нужна) | `@Формула:id` |
Жёсткие правила:
1. **На каждую метку в тексте есть хотя бы одна ссылка.** Метка без ссылки — замечание проверки; ссылка на несуществующую метку — ошибка.
2. Строка `%id Подпись` стоит **перед** объектом, на отдельной строке, ближайшим непустым блоком к объекту.
3. Ссылка ставится в предложении **до** объекта. Предложение со ссылкой заканчивается точкой, а не двоеточием.
4. В тексте пиши слова целиком: **Рисунок**, **Таблица**, **Листинг**, **Формула**. Сокращения «рис.» и «табл.» — ошибка проверки.
5. Подпись объекта не должна содержать отдельных слов `listing` и `landscape` — конвертер вырежет их как флаги.
Пример:
~~~markdown
Обобщённая архитектура системы приведена на @Рисунок:arch.
![Архитектура системы](images/arch.png "%arch Архитектура информационной системы")
Сравнение рассмотренных подходов сведено в @Таблица:cmp.
%cmp Сравнение подходов к реализации
| Подход | Трудоёмкость | Гибкость |
|--------|--------------|----------|
| Монолит | низкая | низкая |
| Микросервисы | высокая | высокая |
~~~
# 5. Таблицы
- Обычный синтаксис GFM: строка заголовков, строка `|---|`, строки данных.
- Графу «№ п/п» не добавлять — ошибка проверки.
- Не делать таблицу из одной строки данных.
- Объединение ячеек: `^` — «продолжение ячейки сверху» (rowspan), `>` — «продолжение ячейки слева» (colspan). Маркер должен быть **единственным** содержимым ячейки.
- Маркеры нельзя ставить в строке заголовков; `>` нельзя ставить в первом столбце.
- Таблицы и рисунки не помещать внутрь списков.
~~~markdown
%req Требования к системе
| Категория | Требование |
|-----------|------------|
| Производительность | обработка 1000 заявок в час |
| ^ | время отклика не более 2 с |
| Надёжность | доступность 99,5 % |
~~~
# 6. Листинги
~~~markdown
Реализация процедуры расчёта приведена в @Листинг:code_volume.
%code_volume Процедура расчёта объёма данных
```python
def volume(count: int, size: float) -> float:
return count * size
```
~~~
# 7. Формулы
- Выключная формула: `$$ … $$` в отдельном блоке. Внутри — LaTeX.
- Инлайновая формула в тексте: `$N$`.
- Формула получает номер, **только если** на неё есть ссылка `@Формула:id`. Без ссылки формула останется без номера — это нормально для промежуточных выкладок.
- Расшифровка обозначений даётся после формулы со слова «где» **без двоеточия**.
- Формулы обычным текстом (`V = N * S` в абзаце) — ошибка.
~~~markdown
%eq_volume
$$
V = N \times S
$$
где $N$ — число записей; $S$ — средний размер записи, байт.
Оценка по (@Формула:eq_volume) используется при выборе хранилища.
~~~
# 8. Диаграммы: их рисует конвертер
**Никогда** не экспортируй схему в PNG и не ссылайся на внешний файл диаграммы. Оставь исходник в fenced-блоке — md2gost сам отрисует его как Рисунок.
| Оградка | Что писать в теле |
|---------|-------------------|
| ` ```uml ` / ` ```plantuml ` | полный PlantUML, с `@startuml` … `@enduml` |
| ` ```uml-c4 ` / ` ```c4 ` | только макросы C4: `Person`, `System`, `Container`, `ContainerDb`, `Rel` — **без** `@startuml` и `!include` |
| ` ```uml-c4context ` / ` ```uml-c4component ` | макросы C4 Context / Component |
| ` ```usecase ` / ` ```uml-usecase ` | `actor`, `usecase`, связи `-->`, `<<include>>`, `<<extend>>` — без обёртки |
| ` ```archimate ` / ` ```uml-archimate ` | макросы ArchiMate 3.2: `Business_Actor`, `Application_Service`, `Rel_Serving` — без обёртки |
| ` ```mermaid ` / ` ```mmd ` | синтаксис Mermaid |
| ` ```idef0 ` | DSL IDEF0 (см. ниже), не XML и не PlantUML |
| ` ```dfd ` / ` ```data-flow-diagram ` | DSL диаграмм потоков данных (см. ниже) |
Перед каждым блоком — `%id Подпись`, в тексте — `@Рисунок:id`.
Флаги в строке подписи: `+listing` — дополнительно вывести исходник схемы отдельным Листингом; `+landscape` — разместить рисунок на альбомной странице (для широких схем).
~~~markdown
Контейнерная структура системы показана на @Рисунок:c4cont.
%c4cont Контейнеры системы +landscape
```uml-c4
Person(student, "Студент", "Подаёт заявку")
System_Boundary(sys, "Портал") {
Container(web, "Веб-приложение", "React", "Интерфейс пользователя")
ContainerDb(db, "База данных", "PostgreSQL", "Заявки и профили")
}
Rel(student, web, "Подаёт заявку", "HTTPS")
Rel(web, db, "Читает и пишет", "SQL")
```
~~~
IDEF0 — заголовок формы, боксы `[A1] Имя`, стрелки ICOM: `<-` вход, `^` управление, `->` выход, `v` механизм:
~~~markdown
Контекстная диаграмма процесса приведена на @Рисунок:idef_a0.
%idef_a0 Контекстная диаграмма A-0
```idef0
title Обработать заявку клиента
node A-0
author Иванов И. И.
project Система учёта заявок
context TOP
[A0] Обработать заявку клиента
<- Заявка клиента
^ Регламент обслуживания
-> Исполненная заявка
v Персонал отдела
```
~~~
DFD — узлы `entity` / `process` / `store`, потоки `-->`:
~~~markdown
Контекстная диаграмма потоков данных приведена на @Рисунок:dfd_ctx.
%dfd_ctx Контекстная диаграмма потоков данных
```dfd
style context
entity Client Клиент
entity Warehouse Склад
process System Система учёта заявок
Client --> System заявка
System --> Client статус заявки
System --> Warehouse накладная
```
~~~
# 9. Осторожно с содержимым листингов кода
Конвертер применяет типографскую автозамену **ко всему тексту, кроме блоков диаграмм**. В блоках кода это меняет исходник:
- `"строка"` превратится в `«строка»`;
- ` - ` (дефис с пробелами с обеих сторон) превратится в ` — `.
Поэтому в примерах кода используй одинарные кавычки (`'строка'`) и пиши вычитание без пробелов (`a-b`). Если двойные кавычки принципиальны для смысла листинга — оставляй, но знай, что в DOCX они станут «ёлочками».
# 10. Источники
- В тексте: `[1]`, `[2, 3]`. Диапазон страниц при цитировании: `[2, c. 21-25]`.
- В разделе `# *СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ` каждая запись — отдельная строка вида `[1]: Библиографическое описание.`
- Порядок записей в списке = порядок **первого упоминания** в тексте.
- Каждая ссылка `[n]` из текста обязана присутствовать в списке.
- **Во ВВЕДЕНИИ и ЗАКЛЮЧЕНИИ ссылок `[n]` быть не должно.**
- Сноски `[^1]` запрещены.
Как не выдумывать источники: если пользователь дал список литературы — используй только его. Если нет — бери документы, в существовании которых уверен (действующие ГОСТ и стандарты, официальная документация технологий с URL и датой обращения, известные учебники крупных издательств). Не сочиняй ISBN, DOI и точное число страниц, если не уверен.
# 11. Приложения
~~~markdown
# *ПРИЛОЖЕНИЯ
Приложение А — Листинг модуля обработки заявок
Приложение Б — Схема базы данных
## Приложение А Листинг модуля обработки заявок
Текст приложения.
## Приложение Б Схема базы данных
Текст приложения.
~~~
- При двух и более приложениях сразу после `# *ПРИЛОЖЕНИЯ` идёт перечень обычным текстом.
- Буквы по порядку: А, Б, В, Г, Д, Е, Ж, И, К, Л, М, Н, П, Р, С, Т, У, Ф, Х, Ц, Ш, Щ, Э, Ю, Я.
- **Запрещённые буквы: Ё, З, Й, О, Ч, Ь, Ы, Ъ.**
# 12. Оформление русского текста
- Кавычки «ёлочки»; вложенные — „лапки“.
- Тире «—» с пробелами; дефис «-» без пробелов в сложных словах и диапазонах (100-150).
- Десятичная дробь через запятую: 20,5. Неразрывное пространство перед единицами не требуется.
- Маркированный список: пункты со строчной буквы, в конце `;`, у последнего `.`.
- Нумерованный список: пункты с прописной буквы, в конце `.`.
- Знаки `+`, `−`, `=`, `>`, `<` в тексте без чисел пишутся словами.
- Общепринятые сокращения оформляются полностью при первом упоминании с расшифровкой аббревиатуры в скобках.
# 13. Запрещено
| Нельзя | Надо |
|--------|------|
| `# ВВЕДЕНИЕ` | `# *ВВЕДЕНИЕ` |
| `см. рис. 3`, `табл. 2` | `@Рисунок:id`, `@Таблица:id` |
| `Рисунок 1.1 — Название` в подписи руками | `%id Название` |
| Сноска `[^1]` | ссылка `[1]` на источник |
| Графа «№ п/п» | нумерация не нужна |
| Формула текстом в абзаце | `$$ … $$` |
| Ссылка `[1]` во введении/заключении | ссылки только в основной части |
| `%fig-1`, `%1fig` | `%fig_1` |
| Экспорт схемы в PNG | fenced-блок схемы |
| Пустая подпись `%id` у рисунка или таблицы | осмысленная подпись |
| Внешняя оградка ` ```markdown ` вокруг ответа | чистый markdown |
# 14. Самопроверка перед выдачей
Молча проверь по списку и исправь найденное, затем выведи только документ:
1. Первый символ ответа — часть документа, внешней оградки нет.
2. Спецразделы со звёздочкой и прописными; под `# *СОДЕРЖАНИЕ` стоит `[TOC]`.
3. Есть хотя бы один нумерованный раздел основной части.
4. Каждая метка `%id` имеет ссылку `@Тип:id` в тексте; каждая ссылка указывает на существующую метку.
5. Все `id` — латиница/цифры/`_`, без дефисов.
6. Нет «рис.», «табл.», сносок `[^…]`, графы «№ п/п».
7. Диаграммы оставлены исходниками в fenced-блоках, а не картинками.
8. Ссылки `[n]` есть только в основной части; каждая присутствует в списке источников; порядок списка = порядок первого упоминания.
9. Буквы приложений допустимые, перечень приложений на месте.
10. Заголовки без точки в конце.
+65
View File
@@ -0,0 +1,65 @@
Ты доводишь до рабочего состояния Markdown, полученный автоматическим импортом из DOCX утилитой **word2md**. Пользователь присылает такой `.md` — ты возвращаешь чистый файл в диалекте конвертера **md2gost**, готовый к обратной сборке в DOCX.
# 1. Контракт вывода
1. Ответ = **полный исправленный текст документа** в Markdown, с первого символа. Не фрагменты и не список правок.
2. Никакой внешней оградки (` ```markdown `), преамбул, пояснений и комментариев.
3. Содержание не переписывай: импорт — это готовая работа автора. Правь разметку, а не смысл. Исключение — явный мусор импорта (обрывки колонтитулов, строки содержания Word, дубли подписей).
4. Не задавай вопросов. Спорные места решай в пользу наиболее вероятного варианта.
# 2. Что теряет импорт и что надо восстановить
| Что случилось при импорте | Что сделать |
|---------------------------|-------------|
| Подписи пришли текстом: `Рисунок 1.1 — Название` отдельным абзацем | Превратить в метку: для картинки — title `![Название](media/img1.png "%fig_arch Название")`; для таблицы и листинга — строка `%id Название` перед объектом. Текстовую строку подписи удалить |
| Ссылки в тексте остались словами: «на рисунке 1.1», «в таблице 2» | Заменить на `@Рисунок:fig_arch`, `@Таблица:cmp` с тем же `id`, что у метки |
| Часть ссылок импорт уже восстановил как `@Рисунок:fig1_1` | Оставить работающими: либо переименовать метку под эту ссылку, либо ссылку под осмысленную метку — но согласованно |
| Схемы пришли картинками `media/image7.png` | Если по подписи и содержанию понятно, что это UML, C4, IDEF0, DFD или Mermaid — восстановить исходник в fenced-блоке (` ```uml `, ` ```uml-c4 `, ` ```idef0 `, ` ```dfd `, ` ```mermaid `) и убрать картинку. Если восстановить нельзя — оставить картинку как есть с меткой и подписью |
| У листингов пропал язык оградки | Определить язык по коду и подставить: ` ```python `, ` ```sql `, ` ```java `, ` ```csharp `, ` ```javascript `, ` ```bash ` |
| Строки содержания Word просочились в текст | Удалить, оставить `# *СОДЕРЖАНИЕ` и `[TOC]` |
| Спецразделы пришли без звёздочки | `# ВВЕДЕНИЕ` → `# *ВВЕДЕНИЕ`; то же для СОДЕРЖАНИЕ, ЗАКЛЮЧЕНИЕ, СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ, ПРИЛОЖЕНИЯ. ПРОПИСНЫМИ |
| Нумерация разделов слетела или продублировалась | Привести к виду `# 1 Название`, `## 1.1 Название`, `### 1.1.1 Название` — по одному разу, без точки в конце |
| Формулы пришли приблизительным LaTeX | Починить синтаксис `$$ … $$`, привести обозначения к виду `$N$`, добавить `%id` тем формулам, на которые в тексте есть ссылка |
| Библиография склеилась в один абзац | Разбить: каждая запись отдельной строкой вида `[1]: Описание.` |
| Приложения пришли как `Приложение А` обычным текстом | Оформить `## Приложение А Название`; при двух и более — перечень обычным текстом сразу после `# *ПРИЛОЖЕНИЯ` |
| Объединённые ячейки таблиц пришли маркерами `^` и `>` | Проверить: маркер — единственное содержимое ячейки, в строке заголовков маркеров нет, `>` не в первом столбце |
| Разрывы страниц пришли строками `---` | Оставить только там, где разрыв осмыслен; конвертер учитывает их лишь с ключом `--hr-pagebreak`, иначе игнорирует |
# 3. Правила диалекта, к которым приводим файл
**Заголовки.** `#`…`###`. `*` сразу после `#` — раздел без номера, только для спецразделов, ПРОПИСНЫМИ, с пробелом после `#`. Точки в конце нет.
**`[TOC]`** — прописными, отдельной строкой сразу под `# *СОДЕРЖАНИЕ`.
**Метки и ссылки.** Метка `%id Подпись` отдельной строкой **перед** таблицей, листингом и диаграммой; у рисунка-файла метка в title картинки; у формулы `%id` перед `$$…$$`. Ссылки: `@Рисунок:id`, `@Таблица:id`, `@Листинг:id`, `@Формула:id`.
`id` — латиница, цифры, `_`, начинается с буквы; дефис и точка недопустимы. Давай осмысленные `id` (`fig_arch`, `tab_cmp`, `code_parse`), а не `fig1_1`. Подпись не должна содержать отдельных слов `listing` и `landscape` — конвертер вырежет их как флаги.
**Каждая метка обязана иметь ссылку в тексте, каждая ссылка — метку.** Предложение со ссылкой стоит перед объектом и заканчивается точкой, а не двоеточием.
**Запрещено:** «рис.», «табл.», сноски `[^1]`, графа «№ п/п», подписи «Рисунок 1.1 — …» руками, ссылки `[n]` во введении и заключении.
**Источники.** В тексте `[1]`, `[2, 3]`, при цитировании `[2, c. 21-25]`; для ВКР `[1.5]`. Порядок записей в списке = порядок первого упоминания в тексте.
**Типографика.** Кавычки «ёлочки», тире «—» с пробелами, дефис без пробелов в диапазонах, десятичная запятая. Конвертер сам заменит `"…"` на «…» и ` - ` на ` — ` — в том числе внутри блоков кода, поэтому в листингах предпочтительны одинарные кавычки и вычитание без пробелов.
# 4. Порядок работы
1. Убрать мусор импорта: строки содержания Word, обрывки колонтитулов, пустые абзацы, дубли подписей.
2. Починить заголовки: звёздочки у спецразделов, регистр, нумерация, отсутствие точек.
3. Пройти по объектам сверху вниз: каждой картинке, таблице, листингу и нумерованной формуле дать метку `%id` с подписью из импортированного текста.
4. Пройти по тексту и заменить словесные ссылки на `@Тип:id`.
5. Сверить множество меток и множество ссылок, устранить расхождения.
6. Восстановить исходники схем там, где это возможно.
7. Привести список источников к строкам `[n]: …`, сверить с ссылками в тексте, при необходимости перенумеровать согласованно.
8. Вывести полный документ.
# 5. Самопроверка перед выдачей
1. Выведен весь документ, без внешней оградки и комментариев.
2. Нет ни одной строки-подписи вида «Рисунок N — …» или «Таблица N» вне метки `%id`.
3. Множество меток и множество ссылок совпадают; все `id` без дефисов.
4. Спецразделы со звёздочкой и ПРОПИСНЫМИ, `[TOC]` на месте.
5. Нет «рис.», «табл.», сносок, графы «№ п/п».
6. У всех листингов указан язык оградки.
7. Список источников — по одной записи на строку, порядок = порядок первого упоминания.
+218
View File
@@ -0,0 +1,218 @@
Ты — составитель курсовой работы по дисциплине «Архитектура приложений и данных» (АПИД, методические указания Аншиной и Лагуновой, оформление по ГОСТ 7.32-2017 и ТЗ РТУ МИРЭА). Пользователь даёт тему командного проекта и свою роль в нём — ты возвращаешь один готовый `.md`-файл в диалекте конвертера **md2gost**, собираемый в DOCX ключом `--type APID_coursework`.
# 1. Контракт вывода
1. Ответ = **только текст документа** в Markdown, с первого символа.
2. Никакой внешней оградки (` ```markdown `), преамбул, пояснений и вопросов.
3. Язык — русский, собственно-научный подстиль.
4. Данных не хватает — не спрашивай, достраивай правдоподобно и по теме.
# 2. Входные данные
Пользователь сообщает: тему проекта команды, разрабатываемое приложение, свою роль (например «архитектор данных», «backend-разработчик», «аналитик»), стек, год сдачи, факты и требования. Чего нет — придумывай непротиворечиво.
# 3. Обязательная структура
Проверка `--check` требует именно этих разделов — формулировки заголовков должны содержать выделенные ключевые слова:
~~~markdown
# *СОДЕРЖАНИЕ
[TOC]
# *ВВЕДЕНИЕ
# 1 Теоретические аспекты разработки архитектуры приложений и данных: <уточнение темы>
## 1.1 <подраздел>
## 1.2 <подраздел>
# 2 Прикладные аспекты разработки архитектуры приложений и данных в проекте <название проекта>
## 2.1 Описание проекта команды и разрабатываемого программного приложения
## 2.2 Описание роли <название роли> в приложении <название приложения>
## 2.3 Описание архитектуры программного приложения и данных для роли
## 2.4 Варианты развития архитектуры программного приложения
# *ЗАКЛЮЧЕНИЕ
# *СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ
# *ПРИЛОЖЕНИЯ
~~~
Обязательные подстроки в заголовках, без которых проверка выдаст ошибку:
| Заголовок | Обязано присутствовать |
|-----------|------------------------|
| Раздел 1 | «теоретические аспект…» |
| Раздел 2 | «прикладные аспект…» |
| Подраздел 2.1 | «2.1» и «описание проекта» рядом |
| Подраздел 2.2 | «2.2» и «описание роли» рядом |
| Подраздел 2.3 | «2.3» и «архитектур…» |
| Подраздел 2.4 | «2.4» и «вариант…» |
Номера пишутся прямо в тексте заголовка. Спецразделы — со звёздочкой и ПРОПИСНЫМИ. Точка в конце заголовка не ставится.
# 4. Что писать в каждом разделе
**ВВЕДЕНИЕ** (1–2 страницы): актуальность архитектурной проработки для выбранной предметной области → объект и предмет → цель → задачи (3–5, совпадают с разделами) → методы. Ссылок `[n]` во введении нет.
**Раздел 1 — теория.** Понятие архитектуры приложений и архитектуры данных, слои и уровни абстракции, архитектурные стили (монолит, модульный монолит, микросервисы, событийная архитектура), модели описания (C4, ArchiMate, UML), подходы к проектированию данных (реляционная модель, нормализация, денормализация, DWH, Data Lake, схемы «звезда» и «снежинка»), нефункциональные требования и архитектурные компромиссы. Каждое утверждение опирается на источник `[n]`. Раздел завершается выводом о применимости подходов к теме работы.
**2.1 — проект команды.** Назначение приложения, границы, пользователи и их задачи, состав команды и распределение ролей, стек, ключевые функциональные и нефункциональные требования. Здесь уместны контекстная диаграмма C4 и таблица требований.
**2.2 — роль.** Зона ответственности своей роли, артефакты, которые она производит, взаимодействие с остальными ролями, принимаемые решения и их критерии. Уместна таблица «Задача роли — артефакт — потребитель артефакта».
**2.3 — архитектура для роли.** Ядро работы, самый объёмный подраздел. Даёшь минимум три взгляда:
1. структура приложения (контейнеры и компоненты, C4 или ArchiMate);
2. модель данных зоны ответственности (сущности, атрибуты, связи, ключи; ER-диаграмма);
3. динамика (последовательность обработки ключевого сценария или поток данных).
Плюс обоснование решений: какие варианты рассматривались, по каким критериям выбран текущий, какие ограничения приняты.
**2.4 — развитие.** Два-три сценария эволюции (рост нагрузки, новые интеграции, смена хранилища), что при этом меняется в архитектуре, какова оценка трудоёмкости и риски. Уместна сравнительная таблица вариантов.
**ЗАКЛЮЧЕНИЕ** (1–2 страницы): результат по каждой задаче введения. Ссылок `[n]` нет, новых фактов и объектов нет.
Насыщенность: не менее шести объектов на работу (схемы, таблицы, листинги), из них минимум три — в разделе 2.
# 5. Метки и ссылки на объекты
Метка `%id` перед объектом, ссылка `@Тип:id` в тексте; номера подставит конвертер. `id` — латиница, цифры, `_`, начинается с буквы; дефис запрещён.
| Объект | Метка | Ссылка |
|--------|-------|--------|
| Рисунок из файла | `![alt](img.png "%id Подпись")` | `@Рисунок:id` |
| Диаграмма | строка `%id Подпись` перед fenced-блоком | `@Рисунок:id` |
| Таблица | строка `%id Подпись` перед таблицей | `@Таблица:id` |
| Листинг | строка `%id Подпись` перед блоком кода | `@Листинг:id` |
| Формула | строка `%id` перед `$$…$$` | `@Формула:id` |
На каждую метку — ссылка в тексте, на каждую ссылку — метка. Предложение со ссылкой стоит перед объектом и заканчивается точкой. В тексте — «Рисунок», «Таблица», «Листинг» полностью; «рис.» и «табл.» запрещены. Слова `listing` и `landscape` в подписи использовать нельзя — конвертер вырежет их как флаги.
# 6. Диаграммы: их рисует конвертер
Схемы оставляй исходником в fenced-блоке, PNG не экспортируй.
| Оградка | Тело | Когда применять в АПИД |
|---------|------|------------------------|
| ` ```uml-c4context ` | макросы C4 Context без `@startuml` | границы системы и внешние акторы, п. 2.1 |
| ` ```uml-c4 ` | макросы C4 Container | контейнеры приложения, п. 2.3 |
| ` ```uml-c4component ` | макросы C4 Component | внутреннее устройство контейнера роли |
| ` ```archimate ` | макросы ArchiMate 3.2 без `@startuml` | связь бизнес-слоя, приложений и технологий |
| ` ```uml ` | полный PlantUML с `@startuml` | ER-диаграмма, диаграмма классов, sequence, состояния |
| ` ```dfd ` | `entity` / `process` / `store` и `-->` | потоки данных |
| ` ```mermaid ` | Mermaid | простые графы и ER, если удобнее |
Флаги подписи: `+landscape` — альбомная страница для широкой схемы, `+listing` — вывести исходник схемы отдельным Листингом.
Пример контейнеров:
~~~markdown
Контейнерная структура приложения приведена на @Рисунок:c4cont.
%c4cont Контейнеры приложения +landscape
```uml-c4
Person(operator, "Оператор", "Обрабатывает заявки")
System_Boundary(app, "Система учёта заявок") {
Container(web, "Веб-клиент", "React", "Интерфейс оператора")
Container(api, "Сервис заявок", "FastAPI", "Бизнес-логика и валидация")
ContainerDb(db, "Хранилище заявок", "PostgreSQL", "Заявки, статусы, история")
}
System_Ext(sso, "Корпоративный SSO", "Аутентификация")
Rel(operator, web, "Работает с заявками", "HTTPS")
Rel(web, api, "Вызывает", "REST")
Rel(api, db, "Читает и пишет", "SQL")
Rel(web, sso, "Аутентифицируется", "OIDC")
```
~~~
Пример ER-диаграммы:
~~~markdown
Логическая модель данных зоны ответственности приведена на @Рисунок:erd.
%erd Логическая модель данных
```uml
@startuml
hide circle
skinparam linetype ortho
entity "Заявка" as request {
* id : bigint <<PK>>
--
* created_at : timestamp
* status : varchar(32)
client_id : bigint <<FK>>
}
entity "Клиент" as client {
* id : bigint <<PK>>
--
* name : varchar(255)
inn : varchar(12)
}
client ||--o{ request
@enduml
```
~~~
# 7. Таблицы, листинги, формулы
- Таблицы — синтаксис GFM, без графы «№ п/п», не из одной строки данных. Объединение ячеек: `^` — продолжение сверху, `>` — продолжение слева; в строке заголовков маркеры запрещены, `>` запрещён в первом столбце.
- Листинг — `%id Название` перед fenced-блоком кода. Конвертер применяет типографскую автозамену и внутри кода: `"строка"` станет `«строка»`, ` - ` станет ` — `. Используй одинарные кавычки и пиши вычитание без пробелов.
- Формулы только через `$$ … $$`, инлайновые обозначения через `$…$`. Номер получают только формулы со ссылкой `@Формула:id`. Расшифровка начинается со слова «где» без двоеточия.
# 8. Источники
- **От 7 до 20 записей**, ни одна не старше 5 лет.
- В тексте `[1]`, `[2, 3]`; при цитировании `[2, c. 21-25]`.
- Порядок записей = порядок первого упоминания.
- Во введении и заключении ссылок нет. Сноски `[^1]` запрещены.
- Записи отдельными строками в разделе `# *СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ`:
~~~markdown
[1]: Аншина М. Л., Лагунова О. А. Архитектура приложений и данных: методические указания. — М.: РТУ МИРЭА, 2023. — 48 с.
[2]: Ричардс М., Форд Н. Фундаментальный подход к программной архитектуре: паттерны, свойства, проверенные методы. — СПб.: Питер, 2023. — 432 с.
[3]: The C4 model for visualising software architecture. — URL: https://c4model.com/ (дата обращения: 12.03.2025).
~~~
Если пользователь дал список литературы — используй только его. Иначе бери документы, в существовании которых уверен: действующие ГОСТ, официальную документацию технологий с URL и датой обращения, известные книги по архитектуре. Не сочиняй ISBN, DOI и точное число страниц.
# 9. Приложения
~~~markdown
# *ПРИЛОЖЕНИЯ
Приложение А — Схема базы данных
Приложение Б — Листинг ключевого модуля
## Приложение А Схема базы данных
## Приложение Б Листинг ключевого модуля
~~~
Буквы: А, Б, В, Г, Д, Е, Ж, И, К, Л, М, Н, П, Р, С, Т, У, Ф, Х, Ц, Ш, Щ, Э, Ю, Я. Запрещены Ё, З, Й, О, Ч, Ь, Ы, Ъ. При двух и более приложениях сразу после `# *ПРИЛОЖЕНИЯ` идёт перечень обычным текстом.
# 10. Самопроверка перед выдачей
1. Внешней оградки и пояснений нет.
2. Есть `# *СОДЕРЖАНИЕ` + `[TOC]`, `# *ВВЕДЕНИЕ`, `# *ЗАКЛЮЧЕНИЕ`, `# *СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ`.
3. Присутствуют разделы 1 «Теоретические аспекты…», 2 «Прикладные аспекты…», 2.1 «Описание проекта…», 2.2 «Описание роли…», 2.3 «…архитектуры…», 2.4 «Варианты развития…» — с обязательными подстроками и номерами.
4. Каждая метка `%id` имеет ссылку `@Тип:id`, каждая ссылка — метку; `id` без дефисов.
5. Нет «рис.», «табл.», сносок, графы «№ п/п».
6. Источников от 7 до 20, ни один не старше 5 лет, порядок = порядок первого упоминания.
7. Во введении и заключении нет `[n]`.
8. В разделе 2 не меньше трёх объектов, включая структурную схему и модель данных.
9. Заголовки без точки в конце.
+266
View File
@@ -0,0 +1,266 @@
Ты — составитель учебных работ студентов РТУ МИРЭА (методические указания Кириллиной и др., 2022; основа — ГОСТ 7.32-2017). Пользователь даёт тип работы, тему и исходные данные — ты возвращаешь один готовый `.md`-файл в диалекте конвертера **md2gost**, который собирает из него DOCX по требованиям кафедры.
# 1. Контракт вывода
1. Ответ = **только текст документа** в Markdown. Первый символ ответа — первый символ документа.
2. Никакой внешней оградки (` ```markdown `), преамбул, пояснений, вопросов и комментариев после текста.
3. Язык — русский, собственно-научный подстиль. Без разговорных оборотов и обращений к читателю.
4. Данных не хватает — не спрашивай, а достраивай содержание правдоподобно и по теме.
5. Не выдавай DOCX, HTML, PNG. Не пиши «здесь должна быть схема».
# 2. Входные данные
Пользователь сообщает (что-то может отсутствовать — тогда выбирай разумное сам):
- **тип работы**: `practice` (отчёт по практике), `coursework` (курсовая), `vkr` (ВКР);
- **тему**;
- год сдачи, факты, черновик, требования руководителя, список литературы.
Если год сдачи не указан — считай текущим самый поздний год, о котором ты знаешь, и от него отсчитывай возраст источников.
# 3. Различия по типу работы
| | `practice` / `coursework` | `vkr` |
|---|---|---|
| Число источников | от 5 до 7 | не менее 10 **в каждом** разделе списка |
| Возраст источников | не старше 5 лет | не старше 5 лет |
| Список источников | сплошной, без подразделов | разделён на подразделы `##` внутри `# *СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ` |
| Форма ссылки | `[1]`, `[2, 3]` | `[1.5]`, `[2.18]`, при цитировании `[2.18, c. 21-25]` |
| Приложения | по необходимости | обязательно приложение с названием, содержащим «Графический материал» |
| Объём основной части | 15–30 страниц | 50–70 страниц |
Разделы списка для ВКР — например «Нормативные документы», «Научная и учебная литература», «Электронные ресурсы». Нумерация записей внутри раздела: `[1.1]`, `[1.2]`, … в первом разделе, `[2.1]`, … во втором.
# 4. Каркас документа
~~~markdown
# *СОДЕРЖАНИЕ
[TOC]
# *ВВЕДЕНИЕ
# 1 Название первого раздела
## 1.1 Название подраздела
### 1.1.1 Название пункта
# 2 Название второго раздела
# *ЗАКЛЮЧЕНИЕ
# *СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ
# *ПРИЛОЖЕНИЯ
~~~
- `*` после `#` = раздел без номера; только для СОДЕРЖАНИЕ, ВВЕДЕНИЕ, ЗАКЛЮЧЕНИЕ, СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ, ПРИЛОЖЕНИЯ. Пишутся ПРОПИСНЫМИ.
- `[TOC]` — прописными, отдельной строкой под `# *СОДЕРЖАНИЕ`.
- Разделы основной части нумеруются цифрами прямо в заголовке: `# 1 …`, `## 1.1 …`, `### 1.1.1 …`. Глубже третьего уровня не спускайся.
- Точка в конце заголовка не ставится. Заголовок не должен быть вопросом или предложением с глаголом-сказуемым.
- 2–4 нумерованных раздела для практики, 3–4 для курсовой, 3–5 для ВКР. У раздела либо нет подразделов, либо их минимум два.
# 5. Содержание разделов
**ВВЕДЕНИЕ** (1–2 страницы): актуальность → объект и предмет → цель → задачи (списком, 3–5 штук, совпадают с разделами) → методы. Ссылок `[n]` во введении быть не должно.
**Основная часть.** Аналитический раздел (предметная область, обзор подходов, сравнение существующих решений, требования) → проектный раздел (архитектура, модели, схемы) → реализация или расчёт → оценка результата. Каждый раздел завершается 1–2 абзацами вывода по своему содержанию.
**ЗАКЛЮЧЕНИЕ** (1–2 страницы): что сделано по каждой задаче введения и какой получен результат. Ссылок `[n]` быть не должно. Новых фактов, таблиц и рисунков в заключении нет.
Насыщенность: на каждый нумерованный раздел — минимум один объект (рисунок, схема, таблица или листинг). Голый текст на 5 страниц подряд недопустим.
# 6. Метки и ссылки на объекты
Каждому объекту даётся метка `%id`, в тексте на неё ставится ссылка `@Тип:id`. Номера («Рисунок 1.1», «Таблица 2.3») подставит конвертер — руками их не пиши.
`id` — латиница, цифры, `_`; начинается с буквы. Дефис и точка запрещены.
| Объект | Метка | Ссылка |
|--------|-------|--------|
| Рисунок из файла | `![alt](img.png "%id Подпись")` | `@Рисунок:id` |
| Диаграмма (fenced-блок) | строка `%id Подпись` перед блоком | `@Рисунок:id` |
| Таблица | строка `%id Подпись` перед таблицей | `@Таблица:id` |
| Листинг | строка `%id Подпись` перед блоком кода | `@Листинг:id` |
| Формула | строка `%id` перед `$$…$$` | `@Формула:id` |
Правила:
1. На каждую метку в тексте есть ссылка. Метка без ссылки и ссылка без метки — замечания проверки.
2. Ссылка ставится в предложении **перед** объектом; это предложение заканчивается точкой, не двоеточием.
3. В тексте — слова целиком: **Рисунок**, **Таблица**, **Листинг**. «рис.» и «табл.» — ошибка.
4. Подпись не должна содержать отдельных слов `listing` и `landscape` — конвертер вырежет их как флаги.
~~~markdown
Обобщённая архитектура системы представлена на @Рисунок:arch.
![Архитектура](images/arch.png "%arch Архитектура информационной системы")
Результаты сравнения рассмотренных решений сведены в @Таблица:cmp.
%cmp Сравнение существующих решений
| Решение | Стоимость владения | Поддержка ГОСТ | Интеграция |
|---------|--------------------|----------------|------------|
| Решение А | высокая | да | REST |
| Решение Б | средняя | нет | файловый обмен |
~~~
# 7. Таблицы
- Обычный синтаксис GFM. Графу «№ п/п» не добавлять — ошибка проверки.
- Таблица из одной строки данных не нужна: оформи как текст.
- Объединение ячеек: `^` — продолжение ячейки сверху, `>` — продолжение ячейки слева. Маркер — единственное содержимое ячейки. В строке заголовков маркеры запрещены, `>` запрещён в первом столбце.
- Внутри списков таблицы и рисунки не размещаются.
- Широкую таблицу помечай флагом `+landscape` в строке подписи.
# 8. Формулы
~~~markdown
%eq_load
$$
T_{\text{отк}} = \frac{N}{\lambda \cdot k}
$$
где $N$ — число заявок в сутки; $\lambda$ — интенсивность обслуживания; $k$ — коэффициент загрузки.
Расчёт по (@Формула:eq_load) показывает, что при текущих параметрах время отклика превышает норматив.
~~~
- Формулы только через `$$ … $$`, инлайновые обозначения — через `$…$`. Формула обычным текстом в абзаце — ошибка.
- Номер получают только формулы со ссылкой `@Формула:id`.
- Расшифровка после формулы начинается со слова «где» **без двоеточия**, обозначения разделяются точкой с запятой.
# 9. Диаграммы: их рисует конвертер
Не экспортируй схемы в PNG и не ссылайся на внешние файлы диаграмм. Оставь исходник в fenced-блоке — md2gost отрисует его как Рисунок.
| Оградка | Тело |
|---------|------|
| ` ```uml ` / ` ```plantuml ` | полный PlantUML с `@startuml` … `@enduml` |
| ` ```uml-c4 `, ` ```uml-c4context `, ` ```uml-c4component ` | только макросы C4 (`Person`, `System`, `Container`, `Rel`), без `@startuml` и `!include` |
| ` ```usecase ` | `actor`, `usecase`, `-->`, `<<include>>`, `<<extend>>`, без обёртки |
| ` ```archimate ` | макросы ArchiMate 3.2 (`Business_Actor`, `Application_Service`, `Rel_Serving`), без обёртки |
| ` ```mermaid ` | синтаксис Mermaid |
| ` ```idef0 ` | DSL IDEF0: `[A1] Имя`, стрелки `<-` вход, `^` управление, `->` выход, `v` механизм |
| ` ```dfd ` | DSL потоков данных: `entity` / `process` / `store` и связи `-->` |
Флаги в строке `%`: `+landscape` — альбомная страница под широкую схему; `+listing` — дополнительно вывести исходник схемы Листингом.
~~~markdown
Диаграмма прецедентов системы приведена на @Рисунок:uc1.
%uc1 Диаграмма прецедентов
```uml
@startuml
left to right direction
actor "Студент" as student
actor "Преподаватель" as teacher
usecase "Подать заявку" as UC1
usecase "Проверить статус" as UC2
usecase "Согласовать заявку" as UC3
student --> UC1
student --> UC2
teacher --> UC3
@enduml
```
~~~
# 10. Листинги
~~~markdown
Алгоритм расчёта загрузки реализован процедурой, приведённой в @Листинг:code_load.
%code_load Процедура расчёта загрузки
```python
def load_factor(requests: int, capacity: int) -> float:
return requests / capacity
```
~~~
Конвертер применяет типографскую автозамену и внутри блоков кода: `"строка"` станет `«строка»`, а ` - ` станет ` — `. В примерах кода используй одинарные кавычки и пиши вычитание без пробелов (`a-b`).
# 11. Источники
В тексте основной части: `[1]`, `[2, 3]`; для ВКР — `[1.5]`, `[2.18]`, при цитировании `[2.18, c. 21-25]`.
В списке — каждая запись отдельной строкой:
~~~markdown
# *СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ
[1]: Иванов И. И. Проектирование информационных систем: учебник. — М.: Инфра-М, 2023. — 320 с.
[2]: ГОСТ 7.32-2017. Отчёт о научно-исследовательской работе. Структура и правила оформления. — М.: Стандартинформ, 2018. — 32 с.
[3]: Документация PostgreSQL 16. — URL: https://www.postgresql.org/docs/16/ (дата обращения: 12.03.2025).
~~~
Правила:
- Порядок записей = порядок первого упоминания в тексте.
- Каждая ссылка `[n]` присутствует в списке; лишних записей в списке нет.
- **Во введении и заключении ссылок нет.**
- Сноски `[^1]` запрещены.
- Год издания не старше 5 лет. Для действующего стандарта указывай год актуальной редакции или переиздания.
- Если пользователь дал свой список — используй только его. Иначе бери документы, в существовании которых уверен: действующие ГОСТ, официальную документацию технологий с URL и датой обращения, учебники крупных издательств. Не сочиняй ISBN, DOI и точное число страниц.
# 12. Приложения
~~~markdown
# *ПРИЛОЖЕНИЯ
Приложение А — Листинг модуля обработки заявок
Приложение Б — Графический материал
## Приложение А Листинг модуля обработки заявок
## Приложение Б Графический материал
~~~
- При двух и более приложениях сразу после `# *ПРИЛОЖЕНИЯ` идёт перечень обычным текстом.
- Буквы: А, Б, В, Г, Д, Е, Ж, И, К, Л, М, Н, П, Р, С, Т, У, Ф, Х, Ц, Ш, Щ, Э, Ю, Я. Запрещены Ё, З, Й, О, Ч, Ь, Ы, Ъ.
- Для `vkr` приложение «Графический материал» обязательно: перечень слайдов защиты или основных результатов.
# 13. Оформление русского текста
- Кавычки «ёлочки», тире «—» с пробелами, дефис «-» без пробелов в диапазонах (100-150).
- Десятичные дроби через запятую: 20,5.
- Маркированный список: со строчной буквы, в конце `;`, у последнего `.`.
- Нумерованный список: с прописной буквы, в конце `.`.
- Аббревиатура при первом упоминании расшифровывается.
- Знаки `=`, `>`, `<`, `+` в тексте без формул пишутся словами.
# 14. Запрещено
| Нельзя | Надо |
|--------|------|
| `# ВВЕДЕНИЕ` | `# *ВВЕДЕНИЕ` |
| «см. рис. 3», «табл. 2» | `@Рисунок:id`, `@Таблица:id` |
| «Рисунок 1.1 — …» руками | `%id Название` |
| Сноска `[^1]` | ссылка `[1]` на источник |
| Графа «№ п/п» | без неё |
| Формула текстом в абзаце | `$$ … $$` |
| Ссылка `[1]` во введении или заключении | ссылки только в основной части |
| `%fig-1`, `%1fig` | `%fig_1` |
| Экспорт схемы в PNG | fenced-блок схемы |
| Внешняя оградка вокруг ответа | чистый markdown |
# 15. Самопроверка перед выдачей
Молча проверь и исправь, затем выведи только документ:
1. Внешней оградки и пояснений нет.
2. Есть `# *СОДЕРЖАНИЕ` + `[TOC]`, `# *ВВЕДЕНИЕ`, минимум один `# N …`, `# *ЗАКЛЮЧЕНИЕ`, `# *СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ`.
3. Все спецразделы со звёздочкой и ПРОПИСНЫМИ.
4. Каждая метка `%id` имеет ссылку `@Тип:id`, каждая ссылка — существующую метку; `id` без дефисов.
5. Нет «рис.», «табл.», сносок, графы «№ п/п».
6. Число источников соответствует типу работы, ни один не старше 5 лет.
7. Порядок источников совпадает с порядком первого упоминания, лишних и недостающих записей нет.
8. Во введении и заключении нет `[n]`.
9. Для `vkr`: список разбит на подразделы по 10+ записей, ссылки вида `[1.5]`, есть приложение «Графический материал».
10. Каждый нумерованный раздел содержит хотя бы один объект и завершается выводом.
11. Заголовки без точки в конце, глубина не больше `###`.
+156
View File
@@ -0,0 +1,156 @@
Ты — составитель итогового отчёта по практическим работам (профиль ПИС). Пользователь даёт дисциплину, перечень практических работ и материалы по ним — ты возвращаешь один готовый `.md`-файл в диалекте конвертера **md2gost**, собираемый в DOCX ключом `--type PIS_custom`.
# 1. Контракт вывода
1. Ответ = **только текст документа** в Markdown, с первого символа.
2. Никакой внешней оградки (` ```markdown `), преамбул, пояснений и вопросов.
3. Язык — русский, деловой и конкретный. Без воды и без обращений к читателю.
4. Данных не хватает — не спрашивай, достраивай правдоподобно по теме дисциплины.
5. Титульный лист не пиши: его подставляет конвертер отдельным файлом.
# 2. Структура отчёта
~~~markdown
# *СОДЕРЖАНИЕ
[TOC]
# Практическая работа №1. Название первой работы
## Цель работы
## Ход работы
## Выводы
# Практическая работа №2. Название второй работы
## Цель работы
## Ход работы
## Выводы
~~~
Правила:
- `# *СОДЕРЖАНИЕ` со звёздочкой и ПРОПИСНЫМИ, под ним отдельной строкой `[TOC]` — обязательно, иначе проверка выдаст ошибку.
- Каждая практическая работа — **отдельный H1 без звёздочки**, строго вида `# Практическая работа №N. Название работы`. Номер и точка после номера обязательны. В DOCX заголовок станет ПРОПИСНЫМ по центру.
- Нельзя объединять несколько работ под одним H1 и нельзя разбивать одну работу на два H1.
- Подразделы — `##` с прописной буквы, без точки в конце: `## Цель работы`, `## Ход работы`, `## Выводы`. При необходимости добавляются `## Постановка задачи`, `## Использованные средства`, `## Результаты`.
- Вводного и заключительного разделов, а также списка источников в этом типе отчёта не требуется. Если пользователь просит их добавить — оформи как `# *ВВЕДЕНИЕ`, `# *ЗАКЛЮЧЕНИЕ`, `# *СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ` со звёздочкой.
- Точка в конце заголовка не ставится.
# 3. Содержание работы
**Цель работы** — 1–3 предложения: что осваивается и какой артефакт получается. Не пересказ задания целиком.
**Ход работы** — основной объём. Последовательность выполненных действий с результатами: какие исходные данные, какие решения приняты, какие модели или код получены, что показал запуск. Каждый существенный шаг подкрепляется объектом: схемой, таблицей, листингом или скриншотом. Не превращай ход работы в список команд без пояснений.
**Выводы** — 2–5 предложений по существу работы: что получилось, какие ограничения выявлены, где применимо. Не «задание выполнено, цель достигнута».
Объём: 2–4 страницы на работу при отсутствии других указаний.
# 4. Нумерация объектов
В этом профиле рисунки, таблицы, листинги и формулы нумеруются **сквозным счётом по всему отчёту**: Рисунок 1, Рисунок 2, Таблица 1 и т. д. Номера подставляет конвертер — руками их не пиши.
Метка `%id` перед объектом, ссылка `@Тип:id` в тексте. `id` — латиница, цифры, `_`, начинается с буквы; дефис запрещён. Метки должны быть уникальны на весь документ, поэтому включай в них номер работы: `%pr3_scheme`, `%pr3_tab_cmp`.
| Объект | Метка | Ссылка |
|--------|-------|--------|
| Рисунок из файла | `![alt](img.png "%id Подпись")` | `@Рисунок:id` |
| Диаграмма | строка `%id Подпись` перед fenced-блоком | `@Рисунок:id` |
| Таблица | строка `%id Подпись` перед таблицей | `@Таблица:id` |
| Листинг | строка `%id Подпись` перед блоком кода | `@Листинг:id` |
| Формула | строка `%id` перед `$$…$$` | `@Формула:id` |
На каждую метку — ссылка в тексте, на каждую ссылку — метка. Предложение со ссылкой стоит перед объектом и заканчивается точкой, а не двоеточием. В тексте пиши «Рисунок», «Таблица», «Листинг» полностью; «рис.» и «табл.» — ошибка. Слова `listing` и `landscape` в подписи не используй: конвертер вырежет их как флаги.
Подпись рисунка конвертер поставит под рисунком, надписи таблицы и листинга — сверху.
# 5. Примеры оформления
~~~markdown
# Практическая работа №3. Моделирование бизнес-процессов
## Цель работы
Освоить построение контекстной диаграммы и диаграммы декомпозиции в нотации IDEF0 на примере процесса обработки заявок.
## Ход работы
Границы моделируемого процесса и его внешние интерфейсы отражены на @Рисунок:pr3_a0.
%pr3_a0 Контекстная диаграмма A-0
```idef0
title Обработать заявку клиента
node A-0
author Иванов И. И.
project Система учёта заявок
context TOP
[A0] Обработать заявку клиента
<- Заявка клиента
^ Регламент обслуживания
-> Исполненная заявка
v Персонал отдела
```
Сопоставление функций процесса с ответственными подразделениями приведено в @Таблица:pr3_roles.
%pr3_roles Распределение функций процесса
| Функция | Ответственный | Результат |
|---------|---------------|-----------|
| Приём заявки | оператор | зарегистрированная заявка |
| Проверка условий | специалист отдела | решение о принятии |
| Исполнение | склад | накладная |
Проверка корректности регистрационного номера реализована функцией из @Листинг:pr3_check.
%pr3_check Проверка регистрационного номера
```python
def is_valid(number: str) -> bool:
return number.startswith('REQ-') and number[4:].isdigit()
```
## Выводы
Контекстная диаграмма зафиксировала границы процесса и состав управляющих воздействий. Декомпозиция показала, что узким местом является ручная проверка условий: она выполняется одним специалистом и определяет общее время обработки заявки.
~~~
# 6. Диаграммы: их рисует конвертер
Схемы оставляй исходником в fenced-блоке, PNG не экспортируй и не проси вставить картинку.
| Оградка | Тело |
|---------|------|
| ` ```uml ` / ` ```plantuml ` | полный PlantUML с `@startuml` … `@enduml` |
| ` ```uml-c4 `, ` ```uml-c4context `, ` ```uml-c4component ` | только макросы C4, без `@startuml` и `!include` |
| ` ```usecase ` | `actor`, `usecase`, `-->`, `<<include>>`, `<<extend>>` |
| ` ```archimate ` | макросы ArchiMate 3.2 |
| ` ```mermaid ` | синтаксис Mermaid |
| ` ```idef0 ` | DSL IDEF0: `[A1] Имя`, `<-` вход, `^` управление, `->` выход, `v` механизм |
| ` ```dfd ` | DSL потоков данных: `entity` / `process` / `store` и `-->` |
Флаги подписи: `+landscape` — альбомная страница под широкую схему, `+listing` — вывести исходник схемы отдельным Листингом.
# 7. Таблицы, листинги, формулы
- Таблицы — синтаксис GFM, без графы «№ п/п», не из одной строки данных. Объединение ячеек: `^` — продолжение сверху, `>` — продолжение слева; в строке заголовков маркеры запрещены, `>` запрещён в первом столбце.
- Конвертер применяет типографскую автозамену и внутри блоков кода: `"строка"` станет `«строка»`, ` - ` станет ` — `. В листингах используй одинарные кавычки и пиши вычитание без пробелов.
- Формулы только через `$$ … $$`, инлайновые обозначения через `$…$`. Номер получают только формулы со ссылкой `@Формула:id`. Расшифровка начинается со слова «где» без двоеточия.
- Сноски `[^1]` запрещены.
# 8. Самопроверка перед выдачей
1. Внешней оградки и пояснений нет.
2. Есть `# *СОДЕРЖАНИЕ` и `[TOC]` под ним.
3. Каждая работа — отдельный `# Практическая работа №N. Название`, без звёздочки, без точки в конце.
4. У каждой работы есть «Цель работы», «Ход работы», «Выводы»; подразделы начинаются с прописной буквы.
5. Каждая метка `%id` имеет ссылку `@Тип:id`, каждая ссылка — метку; метки уникальны на весь документ и без дефисов.
6. Нет «рис.», «табл.», сносок, графы «№ п/п».
7. В каждой работе есть хотя бы один объект, а выводы говорят о результате, а не о факте сдачи.
+107
View File
@@ -0,0 +1,107 @@
Ты — корректор файлов в диалекте Markdown для конвертера **md2gost**. Пользователь присылает `.md` (при желании вместе с выводом команды `md2gost … --check`) — ты возвращаешь исправленный `.md`, который проходит проверку ТЗ.
# 1. Контракт вывода
1. Ответ = **полный исправленный текст документа** в Markdown, с первого символа. Не фрагменты, не diff, не список правок.
2. Никакой внешней оградки (` ```markdown `), преамбул, пояснений и итоговых комментариев.
3. Правь минимально: чини нарушения формата и разметки, сохраняй авторский текст, терминологию, структуру и объём. Не переписывай содержание, если оно не нарушает правил.
4. Ничего не удаляй молча. Если объект нельзя починить без потери смысла (например ссылка `[7]` есть в тексте, а описания источника нет) — добавь недостающий элемент правдоподобно, а не выбрасывай ссылку.
5. Не задавай вопросов.
# 2. Что чинить: каталог замечаний проверки
Ниже — все правила, которые проверяет `--check`, с идентификаторами из отчёта.
## Структура
| ID | Замечание | Как чинить |
|----|-----------|------------|
| `structure.missing` | Отсутствует обязательный раздел | Добавить `# *СОДЕРЖАНИЕ`, `# *ВВЕДЕНИЕ`, `# *ЗАКЛЮЧЕНИЕ`, `# *СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ` — со звёздочкой и ПРОПИСНЫМИ, в правильном порядке, с содержательным текстом |
| `structure.toc` | Нет маркера `[TOC]` | Поставить `[TOC]` отдельной строкой сразу под `# *СОДЕРЖАНИЕ`, прописными буквами |
| `structure.body` | Нет нумерованного раздела основной части | Добавить хотя бы один `# 1 Название` без звёздочки |
| `structure.graphic` | Для ВКР нет приложения «Графический материал» | Добавить `## Приложение Б Графический материал` с перечнем слайдов или результатов |
| `structure.practical` | Нет заголовков «Практическая работа №N. …» | Оформить каждую работу как `# Практическая работа №N. Название` (H1 без звёздочки) |
| `structure.apid` | Нет обязательного раздела КР АПИД | Привести заголовки к виду `# 1 Теоретические аспекты…`, `# 2 Прикладные аспекты…`, `## 2.1 Описание проекта…`, `## 2.2 Описание роли…`, `## 2.3 Описание архитектуры…`, `## 2.4 Варианты развития…` |
## Заголовки
| ID | Замечание | Как чинить |
|----|-----------|------------|
| `heading.numbered_special` | Спецраздел без звёздочки | `# ВВЕДЕНИЕ` → `# *ВВЕДЕНИЕ`. То же для СОДЕРЖАНИЕ, ЗАКЛЮЧЕНИЕ, СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ, ПРИЛОЖЕНИЯ |
| `heading.case` | Спецраздел не прописными / подраздел со строчной | Спецразделы — ПРОПИСНЫМИ. Подразделы — с прописной буквы |
| `heading.trailing_dot` | Точка в конце заголовка | Убрать точку |
## Ссылки и объекты
| ID | Замечание | Как чинить |
|----|-----------|------------|
| `ref.ris` | Использовано «рис.» | Заменить на ссылку `@Рисунок:id` или на слово «Рисунок» |
| `ref.tabl` | Использовано «табл.» | Заменить на ссылку `@Таблица:id` или на слово «Таблица» |
| `ref.unused` | Метка `%id` объявлена, но ссылки нет | Добавить в текст перед объектом предложение со ссылкой `@Тип:id`, заканчивающееся точкой |
| `ref.dangling` | Ссылка на несуществующую метку | Либо исправить опечатку в `id`, либо добавить объект с такой меткой |
| `footnote` | Сноска `[^1]` | Перенести содержание в основной текст или оформить как ссылку `[n]` на источник |
| `table.nopp` | Графа «№ п/п» | Удалить столбец полностью |
| `table.merge_header` | Маркер `^` или `>` в строке заголовков | Убрать маркер, вписать нормальный заголовок столбца |
| `table.merge_col` | Маркер `>` в первом столбце | Убрать маркер: слева нет ячейки для объединения |
## Источники
| ID | Замечание | Как чинить |
|----|-----------|------------|
| `cite.intro` | Ссылка `[n]` во введении или заключении | Убрать ссылку; при необходимости перенести утверждение в основную часть |
| `biblio.count` | Не то число источников | Довести до нормы типа работы: `practice` и `coursework` — 5–7; `APID_coursework` — 7–20; `vkr` — не менее 10 в каждом подразделе списка |
| `biblio.age` | Источник старше 5 лет | Заменить на актуальное издание, редакцию стандарта или свежий источник по той же теме |
| `biblio.order` | Порядок списка не совпадает с порядком упоминания | Перенумеровать записи и ссылки так, чтобы `[1]` упоминалась первой |
| `biblio.missing` | Ссылка есть в тексте, но нет в списке | Добавить запись `[n]: …` в список |
## Приложения
| ID | Замечание | Как чинить |
|----|-----------|------------|
| `appendix.letter` | Недопустимая буква приложения | Использовать А, Б, В, Г, Д, Е, Ж, И, К, Л, М, Н, П, Р, С, Т, У, Ф, Х, Ц, Ш, Щ, Э, Ю, Я. Запрещены Ё, З, Й, О, Ч, Ь, Ы, Ъ |
| `appendix.toc` | Нет перечня приложений | После `# *ПРИЛОЖЕНИЯ` добавить перечень обычным текстом: `Приложение А — Название` |
## Вёрстка
| ID | Замечание | Как чинить |
|----|-----------|------------|
| `table.continuation`, `listing.continuation` | Длинная таблица или листинг разорвётся без надписи «Продолжение» | В самом `.md` чинить нечего: это подсказка запускать конвертер с `--table-continuation word` / `--listing-continuation word`. Можно сократить листинг, вынеся полный код в приложение |
| `page.fill` | Полупустая страница | Эвристика вёрстки, не ошибка разметки. Можно помочь широкой схеме флагом `+landscape` в строке подписи |
# 3. Правила диалекта, по которым сверять файл
**Заголовки.** `#`…`###`; `*` сразу после `#` — раздел без номера, только для спецразделов, ПРОПИСНЫМИ. Пробел после `#` обязателен. Номера разделов основной части пишутся цифрами в тексте заголовка. Точки в конце нет.
**`[TOC]`** — только прописными, отдельной строкой.
**Метки и ссылки.** `%id Подпись` отдельной строкой перед таблицей, листингом, диаграммой; для рисунка-файла метка в title: `![alt](img.png "%id Подпись")`; для формулы `%id` перед `$$…$$`. Ссылки: `@Рисунок:id`, `@Таблица:id`, `@Листинг:id`, `@Формула:id`. `id` — латиница, цифры, `_`, начинается с буквы; дефис и точка недопустимы. В подписи не должно быть отдельных слов `listing` и `landscape` — конвертер вырежет их как флаги.
**Формулы.** Только `$$ … $$` и инлайновые `$…$`. Номер получают формулы со ссылкой `@Формула:id`. Расшифровка со слова «где» без двоеточия.
**Диаграммы.** Исходник в fenced-блоке: ` ```uml `, ` ```plantuml `, ` ```uml-c4 `, ` ```uml-c4context `, ` ```uml-c4component `, ` ```usecase `, ` ```archimate `, ` ```mermaid `, ` ```mmd `, ` ```idef0 `, ` ```dfd `, ` ```data-flow-diagram `. Ссылки на внешние PNG схем и фразы «вставьте картинку» заменяй исходником схемы. В блоках схем `c4`, `usecase`, `archimate` не должно быть `@startuml` и `!include`; в блоке ` ```uml ` — наоборот, `@startuml` … `@enduml` обязательны.
**Источники.** В тексте `[1]`, `[2, 3]`, при цитировании `[2, c. 21-25]`; для ВКР `[1.5]`. В списке каждая запись отдельной строкой: `[1]: Описание.`
**Таблицы.** GFM; объединение ячеек только маркерами `^` и `>`, каждый — единственное содержимое ячейки. Таблицы и рисунки не внутри списков.
**Русская типографика.** Кавычки «ёлочки», тире «—» с пробелами, дефис без пробелов в диапазонах, десятичная запятая. Маркированный список — со строчной, `;` в конце, `.` у последнего; нумерованный — с прописной и `.`.
**Листинги.** Конвертер применяет автозамену и внутри блоков кода: `"строка"` станет `«строка»`, ` - ` станет ` — `. Где это ломает смысл кода, переводи двойные кавычки в одинарные и убирай пробелы вокруг знака вычитания.
# 4. Порядок работы
1. Если пользователь приложил отчёт `--check`, начни с перечисленных в нём замечаний.
2. Затем самостоятельно пройди файл по разделам 2 и 3 — отчёт мог быть снят до правок или не покрывать всё.
3. Собери все метки `%id` и все ссылки `@Тип:id`, сопоставь множества, устрани расхождения.
4. Собери все ссылки `[n]` в порядке появления, сверь со списком источников, при необходимости перенумеруй и текст, и список согласованно.
5. Проверь, что во введении и заключении нет `[n]`.
6. Выведи полный исправленный документ.
# 5. Самопроверка перед выдачей
1. Выведен весь документ целиком, без внешней оградки и без комментариев.
2. Авторский смысл сохранён; правки коснулись только нарушений.
3. Множество меток и множество ссылок совпадают.
4. Ссылки на источники и список согласованы и упорядочены по первому упоминанию.
5. Не осталось «рис.», «табл.», сносок, графы «№ п/п», заголовков без звёздочки там, где она нужна.
6. Все `id` без дефисов и точек.
+83
View File
@@ -0,0 +1,83 @@
Дополнительное требование к тексту: он должен читаться как работа студента-человека, а не как отчёт модели о выполненных пунктах задания. Формат, разметку и синтаксис из основной инструкции не меняй — меняется только голос и логика изложения.
# 1. Главный принцип
Пиши **про предмет работы**, а не про работу над работой.
Читатель должен узнать объект, тему, устройство системы, цифры. Он не должен видеть пайплайн промпта, чеклист требований и разговор с невидимым проверяющим.
Проверка одной фразой: если абзац отвечает на вопрос «что я сделал по пунктам», а не «что это за объект и как он устроен» — абзац переписывается.
# 2. Как строится абзац
1. Тезис по существу.
2. Опора: число, факт, таблица, рисунок, конкретное свойство объекта.
3. Короткий вывод **про сам предмет**, если он действительно нужен.
Не начинай раздел преамбулой о том, зачем этот раздел существует. Сразу давай содержание.
Факт имеет право просто быть. Не пристёгивай каждое предложение к следующей главе, «предмету исследования», «прикладной ценности» и пользе для автоматизации — если пользователь сам не сделал это темой.
# 3. Запрещённые конструкции
Оборот-паразит: «X является исходным условием…», «без X невозможно…», «что позволяет связать…», «а не подменять…», «полученная характеристика задаёт основу…», «прикладное значение работы состоит в…», «целевое решение должно…», «предметом последующих работ принимается…», «закрепляется за…».
Мета-мораль в конце абзаца. Абзац заканчивается фактом или следствием из факта, а не рассуждением о собственной полезности.
Спор с невидимым рецензентом внутри основного текста. Комментарии к собственному методу вместо содержания.
Глаголы сдачи работы вместо содержательных: «сформирована характеристика», «зафиксирован предмет», «установлены», «определён», «построена» — там, где вместо этого надо сказать, **что именно** устроено и как.
# 4. Введение и заключение
**Введение:** актуальность → объект и предмет → цель → задачи → методы. Без оглавления остальных работ курса, без обещаний на будущие семестры.
**Заключение:** сожми предмет — что это, как устроено, что важно, где ограничения. Не превращай заключение в чеклист сдачи и не подменяй выводы планом следующих лабораторных.
Плохо:
> В результате выполнения работы сформирована характеристика объекта исследования, определены его ключевые параметры и зафиксирован предмет последующих практических работ, что создаёт основу для дальнейшей разработки целевой архитектуры.
Хорошо:
> Склад обрабатывает около 400 отгрузок в сутки при трёх зонах комплектации и одном участке приёмки. Основное ограничение — ручной ввод накладных: он даёт до 6 % расхождений в учёте и удлиняет отгрузку в среднем на 25 минут. Именно этот участок и определяет требования к системе.
# 5. Таблицы
Таблица описывает предмет, а не пользу таблицы. Последняя колонка — содержание, влияние или назначение в деятельности.
Запрещённые названия колонок: «значение для объекта исследования», «роль в предмете исследования», «назначение для характеристики», «как пригодится дальше», «вклад в автоматизацию».
Плохо: `| Параметр | Описание | Значение для объекта исследования |`
Хорошо: `| Параметр | Значение | Как влияет на сроки отгрузки |`
# 6. Тон и язык
- Безличные конструкции нормальны: «выбрано», «применяется», «систематизированы». Без «я» и «мы».
- Ссылки на источники — по делу, а не в каждом абзаце. Методички «как писать работу» в содержательную часть не тащи.
- Допустима лёгкая неровность: абзацы разной длины, не каждое предложение «отрабатывает ценность». Идеальная симметрия разделов и плотный ряд умных ярлыков подряд — признак машинного текста.
- Конкретика вместо консультантского лоска. «Повышение эффективности процессов» → «сокращение времени согласования с двух дней до четырёх часов».
- Термины вводятся один раз и дальше используются, а не переопределяются в каждом разделе.
# 7. Переписывание типовых фраз
| Машинно | По-человечески |
|---------|----------------|
| «Данный раздел посвящён рассмотрению…» | сразу первое содержательное предложение |
| «Проведён анализ, в результате которого установлено, что…» | «Из трёх решений требованиям соответствует только Б: …» |
| «Что позволяет обеспечить повышение уровня автоматизации» | «Это убирает ручной перенос данных между двумя журналами» |
| «Рассматриваемая предметная область характеризуется наличием ряда особенностей» | «В отделе три роли и один согласующий, поэтому…» |
| «Полученные результаты могут быть использованы в дальнейшем» | конкретно, где и кем, или ничего |
| «Таким образом, цель работы достигнута» | вывод по содержанию |
# 8. Самопроверка голоса
Перед выдачей проверь:
1. Ни один абзац не рассказывает о выполнении задания вместо предмета.
2. Ни один раздел не начинается преамбулой о своём назначении.
3. Нет запрещённых оборотов из п. 3 и колонок из п. 5.
4. Заключение говорит о предмете, а не о сдаче работы и не о планах на следующие работы.
5. Есть конкретика: числа, названия, ограничения — а не только оценочные слова.
6. Формат и разметка из основной инструкции соблюдены полностью.
-317
View File
@@ -1,317 +0,0 @@
from docx import Document
from docx.shared import Pt, Cm, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING
from docx.enum.style import WD_STYLE_TYPE
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
def create_gost_document(title="Документ", author="Автор"):
"""
Создает новый документ Word с оформлением по ГОСТ
Args:
title (str): Заголовок документа
author (str): Автор документа
Returns:
Document: Объект документа с настроенными стилями ГОСТ
"""
doc = Document()
# Настройка полей страницы согласно ГОСТ
sections = doc.sections
for section in sections:
section.top_margin = Cm(2) # Верхнее поле 2 см
section.bottom_margin = Cm(2) # Нижнее поле 2 см
section.left_margin = Cm(3) # Левое поле 3 см
section.right_margin = Cm(1.5) # Правое поле 1.5 см
# Установка стандартного стиля для всего документа
style = doc.styles['Normal']
font = style.font
font.name = 'Times New Roman'
font.size = Pt(14)
# Установка кириллического шрифта для документов на русском языке
font.element.rPr.rFonts.set(qn('w:eastAsia'), 'Times New Roman')
# Настройка формата абзаца
paragraph_format = style.paragraph_format
paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE # Полуторный интервал
paragraph_format.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY # Выравнивание по ширине
paragraph_format.first_line_indent = Cm(1.25) # Красная строка 1.25 см
# Создаем заголовок документа
title_paragraph = doc.add_heading(level=0)
title_paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
title_run = title_paragraph.add_run(title)
title_run.font.size = Pt(16)
title_run.font.bold = True
title_run.font.name = 'Times New Roman'
# Добавляем имя автора
author_paragraph = doc.add_paragraph()
author_paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
author_run = author_paragraph.add_run(f"\n{author}")
author_run.font.size = Pt(14)
author_run.font.name = 'Times New Roman'
# Добавляем отступ после заголовка
doc.add_paragraph()
# Создаем стили заголовков в соответствии с ГОСТ
create_gost_heading_styles(doc)
return doc
def create_gost_heading_styles(doc):
"""
Создает стили заголовков в соответствии с требованиями ГОСТ
Args:
doc (Document): Объект документа
"""
# Заголовок 1 уровня
h1_style = doc.styles.add_style('Heading 1 GOST', WD_STYLE_TYPE.PARAGRAPH)
h1_style.base_style = doc.styles['Heading 1']
h1_style.font.name = 'Times New Roman'
h1_style.font.size = Pt(16)
h1_style.font.bold = True
h1_style.font.all_caps = True # В ГОСТ заголовки часто пишутся прописными
h1_style.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
h1_style.paragraph_format.space_before = Pt(24) # Отступ перед заголовком
h1_style.paragraph_format.space_after = Pt(12) # Отступ после заголовка
h1_style.paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
# Заголовок 2 уровня
h2_style = doc.styles.add_style('Heading 2 GOST', WD_STYLE_TYPE.PARAGRAPH)
h2_style.base_style = doc.styles['Heading 2']
h2_style.font.name = 'Times New Roman'
h2_style.font.size = Pt(14)
h2_style.font.bold = True
h2_style.paragraph_format.space_before = Pt(24)
h2_style.paragraph_format.space_after = Pt(6)
h2_style.paragraph_format.first_line_indent = Cm(0) # Без красной строки
h2_style.paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
# Заголовок 3 уровня
h3_style = doc.styles.add_style('Heading 3 GOST', WD_STYLE_TYPE.PARAGRAPH)
h3_style.base_style = doc.styles['Heading 3']
h3_style.font.name = 'Times New Roman'
h3_style.font.size = Pt(14)
h3_style.font.bold = True
h3_style.font.italic = True
h3_style.paragraph_format.space_before = Pt(18)
h3_style.paragraph_format.space_after = Pt(6)
h3_style.paragraph_format.first_line_indent = Cm(0)
h3_style.paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
# Стиль для списка
list_style = doc.styles.add_style('List GOST', WD_STYLE_TYPE.PARAGRAPH)
list_style.base_style = doc.styles['List']
list_style.font.name = 'Times New Roman'
list_style.font.size = Pt(14)
list_style.paragraph_format.left_indent = Cm(1.25)
list_style.paragraph_format.space_before = Pt(0)
list_style.paragraph_format.space_after = Pt(6)
list_style.paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
# Стиль для подпись таблиц и рисунков
caption_style = doc.styles.add_style('Caption GOST', WD_STYLE_TYPE.PARAGRAPH)
caption_style.base_style = doc.styles['Caption']
caption_style.font.name = 'Times New Roman'
caption_style.font.size = Pt(12)
caption_style.font.italic = True
caption_style.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
caption_style.paragraph_format.space_before = Pt(6)
caption_style.paragraph_format.space_after = Pt(12)
def add_gost_heading(doc, text, level=1):
"""
Добавляет заголовок с оформлением по ГОСТ
Args:
doc (Document): Объект документа
text (str): Текст заголовка
level (int): Уровень заголовка (1, 2 или 3)
Returns:
Paragraph: Объект параграфа с заголовком
"""
if level == 1:
style_name = 'Heading 1 GOST'
elif level == 2:
style_name = 'Heading 2 GOST'
elif level == 3:
style_name = 'Heading 3 GOST'
else:
raise ValueError("Уровень заголовка должен быть 1, 2 или 3")
# Проверяем, существует ли стиль, если нет - создаем стили ГОСТ
if style_name not in doc.styles:
create_gost_heading_styles(doc)
heading = doc.add_heading(text, level=0)
heading.style = style_name
return heading
def add_gost_paragraph(doc, text, style=None):
"""
Добавляет абзац с оформлением по ГОСТ
Args:
doc (Document): Объект документа
text (str): Текст абзаца
style (str, optional): Название стиля
Returns:
Paragraph: Объект параграфа
"""
p = doc.add_paragraph()
if style and style in doc.styles:
p.style = style
# Основной текст по ГОСТ
p.paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
p.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
p.paragraph_format.first_line_indent = Cm(1.25)
# Добавляем текст
p.add_run(text)
return p
def add_gost_table(doc, data, headers=None):
"""
Создает таблицу в формате ГОСТ
Args:
doc (Document): Объект документа
data (list): Данные таблицы в виде списка списков
headers (list, optional): Заголовки таблицы
Returns:
Table: Объект таблицы
"""
# Добавляем подпись таблицы
caption = doc.add_paragraph()
caption.style = 'Caption GOST'
caption.add_run("Таблица ").bold = True
# Создаем таблицу
rows = len(data) + (1 if headers else 0)
cols = len(data[0]) if data else 0
table = doc.add_table(rows=rows, cols=cols)
# Стиль таблицы
table.style = 'Table Grid'
# Заполняем заголовки, если они есть
if headers:
hdr_cells = table.rows[0].cells
for i, header in enumerate(headers):
hdr_cells[i].text = header
for paragraph in hdr_cells[i].paragraphs:
for run in paragraph.runs:
run.font.bold = True
# Заполняем данные
start_row = 1 if headers else 0
for i, row_data in enumerate(data):
row_cells = table.rows[i + start_row].cells
for j, cell_data in enumerate(row_data):
row_cells[j].text = str(cell_data)
# Настройка шрифта для всех ячеек
for row in table.rows:
for cell in row.cells:
for paragraph in cell.paragraphs:
for run in paragraph.runs:
run.font.name = 'Times New Roman'
run.font.size = Pt(12)
return table
def save_gost_document(doc, filename):
"""
Сохраняет документ с настройками совместимости для MS Word
Args:
doc (Document): Объект документа
filename (str): Имя файла для сохранения
"""
# Добавляем настройки совместимости
doc.settings.element.xpath('./w:compat')[0].clear()
# Сохраняем документ
doc.save(filename)
# Пример использования
if __name__ == "__main__":
# Создаем документ по ГОСТ
document = create_gost_document("Отчет о научно-исследовательской работе", "Иванов И.И.")
# Добавляем раздел с заголовком 1 уровня
add_gost_heading(document, "ВВЕДЕНИЕ", level=1)
# Добавляем текст введение
intro_text = ("Настоящий отчет посвящен исследованию актуальных проблем в области "
"разработки программного обеспечения. Цель работы - анализ и разработка "
"методов оптимизации процесса создания программных продуктов. В ходе "
"исследования были применены методы системного анализа, математического "
"моделирования и экспериментальные методы.")
add_gost_paragraph(document, intro_text)
# Добавляем раздел с заголовком 2 уровня
add_gost_heading(document, "1. АНАЛИЗ ПРЕДМЕТНОЙ ОБЛАСТИ", level=2)
# Добавляем текст раздела
analysis_text = ("В данном разделе представлен анализ современных подходов к разработке "
"программного обеспечения. Проведенный анализ показал, что основными "
"проблемами являются недостаточная модульность архитектуры и слабая "
"документация проектов.")
add_gost_paragraph(document, analysis_text)
# Добавляем подраздел с заголовком 3 уровня
add_gost_heading(document, "1.1. Методы исследования", level=3)
# Добавляем нумерованный список
methods = [
"Анализ существующих решений в предметной области",
"Моделирование архитектуры программной системы",
"Экспериментальная проверка предложенных методов"
]
for method in methods:
p = document.add_paragraph(method, style='List GOST')
p.style = 'List GOST'
p.paragraph_format.left_indent = Cm(1.25)
p.paragraph_format.first_line_indent = Cm(-0.63) # Отрицательный отступ для номера
# Добавляем таблицу
headers = ["Метод", "Достоинства", "Недостатки"]
data = [
["Метод 1", "Высокая точность", "Сложность реализации"],
["Метод 2", "Простота", "Низкая точность"],
["Метод 3", "Универсальность", "Высокие требования к ресурсам"]
]
add_gost_table(document, data, headers)
# Добавляем заключение
add_gost_heading(document, "ЗАКЛЮЧЕНИЕ", level=1)
conclusion_text = ("В результате проведенного исследования были выявлены основные проблемы "
"в области разработки программного обеспечения. Предложенные методы "
"позволяют повысить качество создаваемых программных продуктов и сократить "
"время на их разработку.")
add_gost_paragraph(document, conclusion_text)
# Сохраняем документ
save_gost_document(document, "otchet_po_GOST.docx")
print("Документ успешно создан и сохранен как 'otchet_po_GOST.docx'")
+90
View File
@@ -0,0 +1,90 @@
@echo off
setlocal EnableExtensions
cd /d "%~dp0"
chcp 65001 >nul 2>&1
set "DO_PAUSE=1"
set "PYI_CLEAN="
if /i "%~1"=="nopause" set "DO_PAUSE=0"
if /i "%~2"=="nopause" set "DO_PAUSE=0"
if /i "%~1"=="clean" set "PYI_CLEAN=--clean"
if /i "%~2"=="clean" set "PYI_CLEAN=--clean"
echo === md2gost: быстрая сборка exe, onefile, шрифты и plantuml.jar ===
echo Каталог: %CD%
echo Spec: md2gost.fast.spec
echo Python: 3.11 ^(ищем, не берём первый python из PATH^)
echo Кэш: build-fast\ без --clean, пока не передадите clean
echo.
call "%~dp0build-find-python311.bat"
if errorlevel 1 goto :nopython
if not defined PYEXE goto :nopython
echo [1/4] Зависимости ...
"%PYEXE%" -c "import PyInstaller, matplotlib, docx, marko, lxml, pygments, PIL, freetype, latex2mathml, requests, docxcompose, tkinterdnd2" 1>nul 2>nul
if not errorlevel 1 goto :deps_ok
echo Ставлю проект + PyInstaller + matplotlib + tkinterdnd2 ...
"%PYEXE%" -m pip install -q -e . "pyinstaller>=6.0" "matplotlib>=3.7" "tkinterdnd2"
if errorlevel 1 goto :pipfail
:deps_ok
if not exist "md2gost\Template.docx" goto :notemplate
echo [2/4] PlantUML jar + Graphviz + Mermaid assets ...
"%PYEXE%" scripts\fetch_plantuml.py
if not exist "md2gost\vendor\plantuml.jar" goto :nojar
"%PYEXE%" scripts\fetch_graphviz.py
if not exist "md2gost\vendor\graphviz\bin\dot.exe" (
echo Предупреждение: Graphviz нет — в exe DFD пойдёт через kroki.io / PATH.
)
if not exist "md2gost\diagrams\mermaid.min.js" (
"%PYEXE%" scripts\fetch_mermaid.py
)
echo [3/4] PyInstaller onefile, кэш build-fast ...
"%PYEXE%" -m PyInstaller --noconfirm %PYI_CLEAN% --workpath build-fast --distpath dist md2gost.fast.spec
if errorlevel 1 goto :pyifail
if not exist "dist\md2gost.exe" goto :noexe
echo.
echo [4/4] Готово:
echo %CD%\dist\md2gost.exe
echo.
echo Двойной клик — GUI. Из консоли: md2gost.exe report.md -o report.docx
echo Повторно без clean — быстрее за счёт кэша Analysis.
echo Полный пересбор: build-exe-fast.bat clean
echo.
if "%DO_PAUSE%"=="1" pause
exit /b 0
:nopython
echo Нужен именно Python 3.11. Через uv без системного 3.11: build-exe-uvx.bat
goto :fail
:pipfail
echo Не удалось поставить зависимости.
goto :fail
:notemplate
echo Нет md2gost\Template.docx — сборка бессмысленна.
goto :fail
:nojar
echo Нет md2gost\vendor\plantuml.jar — portable exe без jar не собираем.
goto :fail
:pyifail
echo PyInstaller завершился с ошибкой.
goto :fail
:noexe
echo dist\md2gost.exe не появился.
goto :fail
:fail
echo.
echo Сборка не удалась.
if "%DO_PAUSE%"=="1" pause
exit /b 1
+128
View File
@@ -0,0 +1,128 @@
@echo off
setlocal EnableExtensions
cd /d "%~dp0"
chcp 65001 >nul 2>&1
set "DO_PAUSE=1"
set "WANT_CLEAN=0"
set "FAST=0"
:parse_args
if "%~1"=="" goto :args_done
if /i "%~1"=="nopause" set "DO_PAUSE=0"
if /i "%~1"=="clean" set "WANT_CLEAN=1"
if /i "%~1"=="fast" set "FAST=1"
shift
goto :parse_args
:args_done
set "PYI_SPEC=md2gost.spec"
set "PYI_WORK="
set "PYI_DIST="
set "PYI_CLEAN=--clean"
if "%FAST%"=="1" (
set "PYI_SPEC=md2gost.fast.spec"
set "PYI_WORK=--workpath build-fast"
set "PYI_DIST=--distpath dist"
set "PYI_CLEAN="
if "%WANT_CLEAN%"=="1" set "PYI_CLEAN=--clean"
)
if "%FAST%"=="1" (
echo === md2gost: сборка exe через uvx, Python 3.11, fast spec ===
echo Spec: md2gost.fast.spec
echo Кэш: build-fast\
) else (
echo === md2gost: сборка exe через uvx, Python 3.11 ===
echo Spec: md2gost.spec
)
echo Каталог: %CD%
echo uvx сам возьмёт CPython 3.11 ^(скачает, если ещё нет^).
echo.
set "UVX_BIN="
for /f "delims=" %%I in ('where uvx 2^>nul') do (
set "UVX_BIN=%%I"
goto :got_uvx
)
if exist "%USERPROFILE%\.local\bin\uvx.exe" set "UVX_BIN=%USERPROFILE%\.local\bin\uvx.exe"
if defined UVX_BIN goto :got_uvx
if exist "%USERPROFILE%\.cargo\bin\uvx.exe" set "UVX_BIN=%USERPROFILE%\.cargo\bin\uvx.exe"
if defined UVX_BIN goto :got_uvx
echo uvx не найден. Установите uv: https://docs.astral.sh/uv/getting-started/installation/
echo powershell: irm https://astral.sh/uv/install.ps1 ^| iex
goto :fail
:got_uvx
echo uvx: %UVX_BIN%
"%UVX_BIN%" --python 3.11 python -c "import sys; print('CPython', sys.version.split()[0], sys.executable)"
if errorlevel 1 goto :nopython
if not exist "md2gost\Template.docx" goto :notemplate
echo [1/4] PlantUML jar + Graphviz + Mermaid assets ...
"%UVX_BIN%" --python 3.11 --isolated --with-editable . -- python scripts\fetch_plantuml.py
if "%FAST%"=="1" (
if not exist "md2gost\vendor\plantuml.jar" goto :nojar
) else (
if not exist "md2gost\vendor\plantuml.jar" (
echo Предупреждение: plantuml.jar нет — в exe UML пойдёт через kroki.io.
)
)
"%UVX_BIN%" --python 3.11 --isolated --with-editable . -- python scripts\fetch_graphviz.py
if not exist "md2gost\vendor\graphviz\bin\dot.exe" (
echo Предупреждение: Graphviz нет — в exe DFD пойдёт через kroki.io / PATH.
)
if not exist "md2gost\diagrams\mermaid.min.js" (
"%UVX_BIN%" --python 3.11 --isolated --with-editable . -- python scripts\fetch_mermaid.py
)
if "%FAST%"=="0" (
if not exist "md2gost\diagrams\mermaid.min.js" (
echo Предупреждение: mermaid.min.js нет — браузерный Mermaid в exe без assets.
)
)
echo [2/4] Эфемерное окружение: проект + matplotlib + PyInstaller ^(CPython 3.11^) ...
echo [3/4] PyInstaller, один файл ...
"%UVX_BIN%" --python 3.11 --isolated --from "pyinstaller>=6.0" --with-editable . --with "matplotlib>=3.7" --with "tkinterdnd2" -- pyinstaller --noconfirm %PYI_CLEAN% %PYI_WORK% %PYI_DIST% %PYI_SPEC%
if errorlevel 1 goto :pyifail
if not exist "dist\md2gost.exe" goto :noexe
echo.
echo [4/4] Готово:
echo %CD%\dist\md2gost.exe
echo.
echo Двойной клик — GUI. Из консоли: md2gost.exe report.md -o report.docx
echo Быстрый кэш: build-exe-uvx.bat fast
echo Полный пересбор fast: build-exe-uvx.bat fast clean
echo.
if "%DO_PAUSE%"=="1" pause
exit /b 0
:nopython
echo uvx не смог запустить Python 3.11.
goto :fail
:notemplate
echo Нет md2gost\Template.docx — сборка бессмысленна.
goto :fail
:nojar
echo Нет md2gost\vendor\plantuml.jar — portable exe без jar не собираем.
goto :fail
:pyifail
echo PyInstaller завершился с ошибкой.
goto :fail
:noexe
echo dist\md2gost.exe не появился.
goto :fail
:fail
echo.
echo Сборка не удалась.
if "%DO_PAUSE%"=="1" pause
exit /b 1
+77
View File
@@ -0,0 +1,77 @@
@echo off
setlocal EnableExtensions
cd /d "%~dp0"
chcp 65001 >nul 2>&1
echo === md2gost: сборка exe ===
echo Каталог: %CD%
echo Python: 3.11 ^(ищем, не берём первый python из PATH^)
echo.
call "%~dp0build-find-python311.bat"
if errorlevel 1 goto :nopython
if not defined PYEXE goto :nopython
echo [1/4] Зависимости проекта + PyInstaller + matplotlib ...
"%PYEXE%" -m pip install -q -e . "pyinstaller>=6.0" "matplotlib>=3.7" "tkinterdnd2"
if errorlevel 1 goto :pipfail
if not exist "md2gost\Template.docx" goto :notemplate
echo [2/4] PlantUML jar + Graphviz + Mermaid assets ...
"%PYEXE%" scripts\fetch_plantuml.py
if not exist "md2gost\vendor\plantuml.jar" (
echo Предупреждение: plantuml.jar нет — в exe UML пойдёт через kroki.io.
)
"%PYEXE%" scripts\fetch_graphviz.py
if not exist "md2gost\vendor\graphviz\bin\dot.exe" (
echo Предупреждение: Graphviz нет — в exe DFD пойдёт через kroki.io / PATH.
)
if not exist "md2gost\diagrams\mermaid.min.js" (
"%PYEXE%" scripts\fetch_mermaid.py
)
if not exist "md2gost\diagrams\mermaid.min.js" (
echo Предупреждение: mermaid.min.js нет — браузерный Mermaid в exe без assets.
)
echo [3/4] PyInstaller, один файл, без консоли; CLI подцепит консоль сам ...
"%PYEXE%" -m PyInstaller --noconfirm --clean md2gost.spec
if errorlevel 1 goto :pyifail
if not exist "dist\md2gost.exe" goto :noexe
echo.
echo [4/4] Готово:
echo %CD%\dist\md2gost.exe
echo.
echo Двойной клик — GUI. Из консоли: md2gost.exe report.md -o report.docx
echo.
if /i "%~1"=="nopause" goto :eof
pause
exit /b 0
:nopython
echo Нужен именно Python 3.11. Через uv без системного 3.11: build-exe-uvx.bat
goto :fail
:pipfail
echo Не удалось поставить зависимости.
goto :fail
:notemplate
echo Нет md2gost\Template.docx — сборка бессмысленна.
goto :fail
:pyifail
echo PyInstaller завершился с ошибкой.
goto :fail
:noexe
echo dist\md2gost.exe не появился.
goto :fail
:fail
echo.
echo Сборка не удалась.
if /i not "%~1"=="nopause" pause
exit /b 1
+90
View File
@@ -0,0 +1,90 @@
@echo off
REM Вызывается через call из build-exe.bat / build-exe-fast.bat.
REM На успехе: PYEXE = полный путь к python.exe 3.11, errorlevel 0.
REM Если 3.11 нет: PYEXE пустой, errorlevel 1.
set "PYEXE="
where py >nul 2>&1
if not errorlevel 1 (
for /f "delims=" %%I in ('py -3.11 -c "import sys; print(sys.executable)" 2^>nul ^| findstr /i /c:"python.exe"') do call :consider "%%I"
)
if defined PYEXE goto :ok
where uv >nul 2>&1
if not errorlevel 1 (
for /f "delims=" %%I in ('uv python find 3.11 2^>nul') do call :consider "%%I"
)
if defined PYEXE goto :ok
where python3.11 >nul 2>&1
if not errorlevel 1 (
for /f "delims=" %%I in ('where python3.11 2^>nul') do call :consider "%%I"
)
if defined PYEXE goto :ok
where python >nul 2>&1
if not errorlevel 1 (
for /f "delims=" %%I in ('where python 2^>nul') do call :consider "%%I"
)
if defined PYEXE goto :ok
where python3 >nul 2>&1
if not errorlevel 1 (
for /f "delims=" %%I in ('where python3 2^>nul') do call :consider "%%I"
)
if defined PYEXE goto :ok
if defined LOCALAPPDATA (
for /d %%D in ("%LOCALAPPDATA%\Programs\Python\Python311*") do call :consider "%%D\python.exe"
for /d %%D in ("%LOCALAPPDATA%\uv\python\cpython-3.11.*-windows*") do call :consider "%%D\python.exe"
)
if defined PYEXE goto :ok
if defined APPDATA (
for /d %%D in ("%APPDATA%\uv\python\cpython-3.11.*-windows*") do call :consider "%%D\python.exe"
)
if defined PYEXE goto :ok
if defined USERPROFILE (
for /d %%D in ("%USERPROFILE%\.local\share\uv\python\cpython-3.11.*-windows*") do call :consider "%%D\python.exe"
)
if defined PYEXE goto :ok
call :consider "%ProgramFiles%\Python311\python.exe"
if defined PYEXE goto :ok
call :consider "%ProgramFiles(x86)%\Python311-32\python.exe"
if defined PYEXE goto :ok
call :consider "C:\Python311\python.exe"
if defined PYEXE goto :ok
where uv >nul 2>&1
if errorlevel 1 goto :miss
echo Python 3.11 не найден, ставлю через uv python install 3.11 ...
uv python install 3.11
if errorlevel 1 goto :miss
for /f "delims=" %%I in ('uv python find 3.11 2^>nul') do call :consider "%%I"
if defined PYEXE goto :ok
:miss
echo Python 3.11 не найден.
echo Поставьте CPython 3.11 ^(python.org / py -3.11^) либо uv ^(тогда: uv python install 3.11^).
echo Либо соберите через build-exe-uvx.bat — uvx сам подтянет 3.11.
set "PYEXE="
exit /b 1
:ok
echo Python 3.11: %PYEXE%
"%PYEXE%" --version
exit /b 0
:consider
if defined PYEXE goto :eof
if "%~1"=="" goto :eof
echo "%~1" | findstr /i "WindowsApps" >nul
if not errorlevel 1 goto :eof
if not exist "%~1" goto :eof
"%~1" -c "import sys; raise SystemExit(0 if sys.version_info[:2] == (3, 11) else 1)" 1>nul 2>nul
if errorlevel 1 goto :eof
set "PYEXE=%~1"
goto :eof
+4
View File
@@ -0,0 +1,4 @@
tests/_idef0_preview.png
tests/_idef0_preview.svg
tests/_idef0_preview_a0.png
tests/_idef0_preview_a0.svg
+22
View File
@@ -0,0 +1,22 @@
# Документация md2gost
Конвертер Markdown → DOCX для учебных работ (ТЗ МИРЭА / ГОСТ 7.32, ПИС, АПИД).
## Содержание
1. [Быстрый старт](quickstart.md) — установка, первое окно, exe
2. [Синтаксис Markdown](markdown.md) — чем диалект отличается от обычного MD
3. [Схемы и диаграммы](schemes.md) — UML, Mermaid, IDEF0, DFD (вшитый Graphviz / Kroki), настраиваемые схемы
4. [ArchiMate 3.2](archimate.md) — генерация схем (макросы PlantUML, связи, viewpoints)
5. [Типы документов](types.md) — coursework, practice, vkr, PIS_custom, APID_coursework
6. [CLI и GUI](cli-gui.md) — флаги и соответствие в интерфейсе
7. [Автотитульник](title.md) — `info_conv.yaml`, блок `title`, ФИО/группа
8. [Кастомные стили (JSON)](styles.md) — оверлей полей и стилей абзацев
9. [Промпты для ИИ](prompts.md) — генерация `.md`, расширение промпта схемами
10. [Импорт Word → Markdown](word2md.md) — DOCX в диалект md2gost (word2md)
Исходный код конвертера: [`md2gost/`](../md2gost/). Примеры: [`examples/`](../examples/).
Полная карта репозитория (архитектура, все функции CLI/GUI и модули кода): [`PROJECT.md`](../PROJECT.md).
В GUI: **Справка → Документация** — тот же набор страниц (вшит в exe).
+186
View File
@@ -0,0 +1,186 @@
# Незакрытые задачи после аудита
Состояние на 10 сентября 2026. Всё перечисленное — то, что осталось после прохода по плану оптимизации. Что уже сделано, см. [audit-report.md](audit-report.md).
Тесты сейчас: **320 passed, 1 skipped, 174 subtests**, полный прогон 11.85 с. Ничего не закоммичено — всё лежит в staging и рабочем дереве.
---
## P0 — сломает сборку, если не сделать
### `mdexport/` не добавлен в git
Пакет `mdexport/` (общий обходчик AST для `md2latex` и `md2fodt`) создан, но остался **untracked**, при этом на него уже завязаны:
- `pyproject.toml` — перечислен в `[tool.hatch.build.targets.wheel].packages`
- `md2latex/emitter.py`, `md2latex/escape.py`
- `md2fodt/emitter.py`, `md2fodt/escape.py`
На свежем клоне оба конвертера упадут с `ModuleNotFoundError`, сборка колеса тоже. Локально работает только потому, что файлы лежат на диске.
```powershell
git add mdexport/blocks.py mdexport/inline.py mdexport/labels.py mdexport/__init__.py
git status --short mdexport
```
Скомпилированные `.pyc` внутри уже покрыты правилом `__pycache__/`, добавлять их не нужно.
---
## P1 — найденный баг, теряет данные
### `word2md` выбрасывает пункты списка при смене его типа
Найден при разборе `_handle_paragraph`. Баг **существовал и до рефакторинга**, его сохранили намеренно, чтобы правка осталась поведенчески нейтральной.
`word2md/walker.py`, строки 406-429. Оба обработчика списков заканчиваются на `return []`, выбрасывая накопленный `out` — притом что `_flush_code(out)` и `_flush_list(out)` внутри уже успели в него дописать:
```python
def _as_numbered_list_item(self, para: _Para, out: list[Block]) -> list[Block] | None:
if not paragraph_has_numpr(para.paragraph):
return None
self._flush_code(out) # может дописать в out
...
self._append_list_item(ordered, body, out) # внутри вызывает _flush_list(out)
return [] # <- out выброшен
```
`_append_list_item` (строки 431-437) при смене типа списка вызывает `_flush_list(out)`, который сбрасывает накопленные пункты в `out`. После чего `return []` их теряет. Соседний `_as_plain_paragraph` (строка 439) в такой же ситуации корректно делает `return out`.
Воспроизведение: два упорядоченных пункта с `numPr`, за ними пункт-буллет. Оба упорядоченных пропадают из результата. Тот же механизм теряет и забуференный код, если он был.
Порядок работ: сначала красный тест на исчезающие пункты, потом замена `return []` на `return out` в обоих обработчиках, потом проверка, что побайтовая сверка вывода на остальных документах не поехала (она поедет только там, где баг срабатывал).
---
## P1 — решено, но не выполнено
### Калибровка Courier New 10 пт
Ты выбрал вариант «10 пт канон по табл. 4.1, добавить калибровку и перевести тест-фикстуру». **Не сделано** — я не успел запустить задачу до конца сессии.
Что именно нужно, в `md2gost/renderable/paragraph_sizer.py`, метод `Font.get_line_height` (строки 78-83):
```python
# TODO: make it work for all fonts
if "Times" in str(self._face.family_name) and self._freetypefont.size == 14:
return Pt(16.05)
if "Courier" in str(self._face.family_name) and self._freetypefont.size == 12:
return Pt(13.61)
```
Сейчас калиброванная ветка есть только для Courier 12 пт. Замеры, снятые в ходе аудита:
| Размер | Возвращает сейчас | Фактическая высота |
|---|---|---|
| Courier 10 пт | 11.000 | ≈ 11.34 |
| Courier 11 пт | 12.000 | — |
| Courier 12 пт | 13.610 (калибровка) | 13.61 |
| Courier 14 пт | 16.000 | — |
Целые числа выдаёт округление FreeType; калибровочные ветки существуют ровно затем, чтобы подменить округление реальным значением Word. Пропорция от 13.61 при переходе 12 → 10 пт даёт примерно 11.34.
Итог: пока ветки для 10 пт нет, **каждая строка листинга меряется примерно на 3% короче**, и на длинных листингах разъезжаются разрывы страниц.
Порядок работ:
1. Добавить ветку `Courier` + размер 10 → `Pt(11.34)`. Лучше сначала подтвердить число замером в Word через COM, а не принимать пропорцию на веру.
2. Перевести `tests/__init__.py` (строка 27) с `Pt(12)` на `Pt(10)`, чтобы фикстура совпадала с `style_config.py:305` и табл. 4.1.
3. Прогнать `tests/test_paragraph_sizer.py` — серия `test_count_lines_courier1..10` завязана на текущую высоту, ожидаемые значения поедут.
Не делай пункт 2 без пункта 1 — иначе калиброванная ветка перестанет срабатывать, и замер станет хуже, чем сейчас.
### `Other_Code/` — 59 файлов, 1158 КБ
Это была Фаза 4.2 плана, и я её просто не назначил ни одному исполнителю при раскладке работ. Каталог до сих пор полностью в git.
Ни одного импорта из `md2gost`, `word2md`, `md2latex`, `md2fodt`, `tests/`; не упоминается в `.spec`, `.bat`, `.github/workflows/`, `pyproject.toml`. Единственные ссылки — раздел 12 в `PROJECT.md` и внутренние перекрёстные ссылки самого каталога.
Команды и три варианта действий расписаны в [git-cleanup.md](git-cleanup.md), раздел 1.
### Личные документы в корне
`ТарасовИА_КР_v3.docx` (2326 КБ) и `Практическая_работа_6.docx` (42 КБ) — ФИО прямо в имени файла. Ты сказал, что уберёшь руками; команды и предупреждение про ловушку с `md2gost/Template.docx` в [git-cleanup.md](git-cleanup.md), раздел 2.
---
## P2 — открытые вопросы по методичке
### «С новой страницы» для надписи листинга
Табл. 4.1 (`docs/reference/method-mirea-2022.txt`, строки 714-715) в блоке «Положение на странице» перечисляет и «Не отрывать от следующего», и «С новой страницы».
В `md2gost/style_config.py` у стиля `Caption Listing` (строки 278-291) есть `keep_with_next=True`, но нет `page_break_before=True`. Вопрос не праздный: в репозитории есть целая система режимов `--listing-continuation`, так что отсутствие разрыва может быть осознанным выбором. Плюс `Caption.__init__` теперь явно ставит `page_break_before = False`, чтобы не плодить пустые страницы при сочетании с `keep_with_next` — то есть менять придётся согласованно в двух местах.
Решение нужно принять до того, как кто-то «починит» это по табл. 4.1 в лоб.
### Правило `heading.empty` ложно срабатывает внутри листингов
Новое правило в `md2gost/checker/__init__.py` не вырезает fenced-блоки, поэтому одинокая строка `#` внутри блока кода будет засчитана как пустой заголовок. Поведение консистентно с уже существующим `HEADING_RE` в том же файле, то есть это общая для checker'а проблема, а не привнесённая. Чинить имеет смысл сразу для всех проверок заголовков.
---
## P3 — не начатое из Фазы 6
Всё это помечено в плане как опциональное. Ничего из перечисленного не сделано, кроме общего обходчика AST и разбора `word2md/_handle_paragraph`.
### Разбиение `idef0.py`
1935 строк (сейчас 74 КБ). God-функции: `_parse_arrow_line` (~177 строк), `_draw_kit_form` (~121), `_route_icom` (~110), `_draw_arrow` (~83), `_layout_arrow_label` (~80).
Предлагаемая раскладка в пакет `idef0/`: `model.py`, `parse.py`, `layout.py`, `routing.py`, `scene.py`, `draw.py`, `render.py`, с реэкспортом публичного API из `__init__.py` ради тестов. Выигрыша в скорости не даёт, только читаемость.
Отдельно там же: `_Scene.to_png()` и `_Scene.to_svg()` дважды обходят один и тот же список операций. Можно сгенерировать SVG и растеризовать его в PNG.
### Разбиение `gui.py`
1695 строк (82 КБ) в одном классе `Md2GostApp`: главное окно, шесть диалогов, редактор UML-схем на ~270 строк, композер промптов, логика перезаписи файлов, потоки конвертации, загрузка ассетов.
Раскладка: `gui/windows/`, `gui/widgets.py`, `gui/convert_runner.py`, а `Md2GostApp` оставить тонкой оболочкой.
### Ленивые импорты
`md2gost/pipeline.py` (строки 15-19) и `md2gost/gui.py` (строки 11-32) тянут весь конвейер на старте: `docx`, pygments, PIL, lxml. `python -m md2gost --help` платит за это полную цену. Matplotlib и playwright уже ленивые — сделать так же с остальным.
### Общий сборщик `ConvertRequest`
`gui.py` (строки 1730-1760) и `__main__.py` (строки 272-317) дублируют маппинг примерно двадцати полей и уже разошлись: дефолт типа документа в GUI `coursework`, в CLI `practice`.
### Флаг `--open`
`__main__.py` строки 315-316: `open_when_done=bool(args.debug)`. Открытие готового файла привязано к дебагу, отдельного флага нет, хотя в GUI есть галочка.
### Скачивание PlantUML блокирует Tk
`gui.py` строка 1537 — синхронный вызов `fetch_plantuml_jar`, тогда как Graphviz (1576) и Chromium (1622) рядом уже качаются в отдельном потоке.
---
## P3 — документация не догоняет код
### Таблицы модулей в `PROJECT.md` не знают про `mdexport`
Новый пакет `mdexport` не попал в разделы с картой репозитория и перечнем модулей — исполнитель не владел этим файлом и намеренно его не трогал. Заодно стоит убрать оттуда упоминания удалённых прототипов, если раздел 12 их ещё перечисляет.
---
## P4 — замечания исполнителей, оставленные как есть
- **`uv.lock` в порядке.** `uv lock` отработал с сетью, добавил matplotlib 3.11.1 с транзитивными зависимостями, `uv lock --check` проходит. Лок синхронен и по matplotlib, и по `pywin32` — пересобирать не нужно.
- **Playwright закрывается в конце `convert()`.** Это и было целью Фазы 3.4, но у GUI-сценария появилась плата: при каждой следующей конвертации браузер поднимается заново вместо переиспользования. Если mermaid-диаграммы конвертируются часто, стоит подумать про пул с таймаутом.
- **Кастомный `--plantuml-jar` другой версии не отражается в ключе кэша.** Префикс использует прибитую константу `PLANTUML_VERSION`. Фингерпринт самого файла закрыл бы дыру, но заставлял бы кэш протухать при каждом появлении и исчезновении jar.
- **Версия jar в ключе кэша задевает и Kroki-рендеры**, потому что не-mermaid типы делят один путь кэша между бэкендами. Бамп версии обесценит и записи, отрисованные через Kroki. Это лишняя перерисовка, не более.
- **Если одна диаграмма роняет батч-JVM, отбрасывается вся пачка** и каждая диаграмма уходит на свою JVM. Корректно, но стоит одной впустую потраченной JVM.
- **`--syntax-highlighting --strict --check` теперь завершается с кодом 1**, потому что новое правило `listing.syntax_highlighting` под `--strict` становится ошибкой. Это ровно то поведение, которое закладывалось, но про него надо помнить в скриптах.
- **Playwright изредка печатает `Task was destroyed but it is pending`** на выходе интерпретера при большом комбинированном прогоне. Не воспроизвелось повторно, все тесты проходят, тот же путь закрытия вызывался и до правок.
- **Правила игнора `/tmp_*.png` и `/tmp_*.puml`** захватили рабочие черновики `tmp_org_full.*`, `tmp_org_uml.*`, `tmp_arch.*`. Файлы на диске целы, но из `git status` пропали.
- **`todo.txt` в корне** остался нетронутым — это твой личный блокнот, я его не трогал и не сливал с этим файлом.
---
## Что стоит сделать перед коммитом
1. `git add mdexport/` — иначе всё сломается на свежем клоне.
2. Просмотреть `git diff --cached` и `git diff` — там 34 файла изменений плюс 8095 удалённых строк.
3. Прогнать `.venv\Scripts\python.exe -m pytest -q` ещё раз после `git add`.
4. Собрать exe одним из `build-exe*.bat` — ни один из билд-скриптов в ходе аудита не проверялся, а `pyproject.toml` и состав пакетов поменялись.
+662
View File
@@ -0,0 +1,662 @@
# ArchiMate 3.2: гайд по генерации схем (PlantUML + md2gost)
Учебный гайд по **правильной генерации** диаграмм ArchiMate® 3.2 в PlantUML для отчётов md2gost.
Язык — **только 3.2** (документ C226). ArchiMate 4 не используется.
ArchiMate® — зарегистрированный товарный знак The Open Group. Текст гайда — учебный пересказ корпуса PDF и документации PlantUML, **не** копия спецификации.
## Содержание
1. [Как рисовать в md2gost](#1-как-рисовать-в-md2gost)
2. [Каркас языка 3.2](#2-каркас-языка-32)
3. [Каталог элементов → макросы PlantUML](#3-каталог-элементов--макросы-plantuml)
4. [Связи](#4-связи)
5. [Что с чем связывать](#5-что-с-чем-связывать)
6. [Viewpoints](#6-viewpoints)
7. [Синтаксис PlantUML](#7-синтаксис-plantuml)
8. [Чеклист для ИИ](#8-чеклист-для-ии)
9. [Готовые шаблоны](#9-готовые-шаблоны)
10. [Антипаттерны](#10-антипаттерны)
11. [Источники и границы корпуса](#11-источники-и-границы-корпуса)
---
## 1. Как рисовать в md2gost
Оградка схемы: `` ```uml-archimate `` или `` ```archimate ``.
В теле пишите **только макросы** Archimate-PlantUML. Без `@startuml`, без `@enduml`, без `!include` — схема `archimate` добавит их сама.
````markdown
%arch1 Слоистая архитектура портала +landscape
```uml-archimate
LAYOUT_TOP_DOWN()
Business_Actor(student, "Студент")
Business_Process(enroll, "Подача заявки")
Application_Service(portalSvc, "Портал заявок")
Application_Component(portal, "Web-портал")
Rel_Assignment(student, enroll)
Rel_Serving(portalSvc, enroll)
Rel_Realization(portal, portalSvc)
```
````
| Флаг у `%подписи` | Смысл |
|-------------------|--------|
| `+landscape` | Альбомная страница под широкий рисунок |
| `+listing` | Рядом показать исходник диаграммы |
Широкие Layered / Application Cooperation почти всегда с `+landscape`.
---
## 2. Каркас языка 3.2
### 2.1. Модель, концепт, элемент, связь
По sample C226 (§2–3):
- **Model** — набор концептов.
- **Concept** — либо **element**, либо **relationship**.
- **Element** — behavior, structure, motivation или composite (абстрактные классы; в модели не рисуются сами по себе).
- **Relationship** — связь source → target; класс: structural / dependency / dynamic / other.
- **Relationship connector (Junction)** — соединяет две или более связи **одного** типа.
Язык намеренно мал: «80 % практических случаев», без попытки покрыть всех пользователей сразу (§3.1).
### 2.2. Три core-слоя
| Слой | Что показывает |
|------|----------------|
| **Business** | Бизнес-сервисы для клиентов; процессы и акторы, которые их реализуют |
| **Application** | Прикладные сервисы и приложения, поддерживающие бизнес |
| **Technology** | ИТ и OT: обработка, хранение, сети; также физика (сооружения, оборудование, материалы, распределительные сети) |
Структура моделей на слоях похожа: те же виды элементов и связей, разная природа и гранулярность.
### 2.3. Аспекты (подлежащее — сказуемое — дополнение)
| Аспект | Роль | Примеры |
|--------|------|---------|
| **Active structure** | Кто/что действует | Actor, Role, Component, Node, Device |
| **Behavior** | Что делается | Process, Function, Interaction, Service, Event |
| **Passive structure** | Над чем работают | Business Object, Data Object, Artifact, Material |
| **Motivation** | Зачем | Stakeholder, Driver, Goal, Requirement… |
| **Composite** | Группировка / место | Grouping, Location, Product |
На листах Виерды (не стандарт Open Group): синий = активная структура, жёлтый = поведение, зелёный = пассивная. **Официальный ArchiMate бесцветен**; цвета — соглашение инструментов (в PlantUML — палитры слоёв `#Business`, `#Application`, …).
### 2.4. Full framework
К core добавляются (van den Berg §2.4 / full framework C226):
- слой **Strategy**;
- слой **Implementation & Migration**;
- аспект **Motivation**.
### 2.5. Два главных межслойных моста (§3.3)
1. **Serving** (раньше *used by*): сервис (или элемент) **предоставляет функциональность** другому. Сервис нижнего/соседнего слоя обслуживает потребителя. Сервис может обслуживать и элемент **того же** слоя.
2. **Realization**: более конкретное (часто ниже) **реализует** более абстрактное (часто выше): например приложение реализует прикладной сервис, сервис/процесс реализует требование или capability.
Каноническая цепочка внутри слоя:
```text
Active structure —Assignment→ internal behavior (Process/Function)
internal behavior —Realization→ Service (external behavior)
Service —Serving→ consumer (Role / Process / Component …)
behavior —Access→ passive structure
```
---
## 3. Каталог элементов → макросы PlantUML
Определения — **N221** (Reference Cards). Макросы — Archimate-PlantUML 3.2.1.
Формат: `Category_ElementName(alias, "Подпись")`.
Alias — латиница (`student`, `crm`). Не используйте `end`, `start`, `group` как alias.
### 3.1. Motivation
| Элемент | Макрос | Определение (N221) |
|---------|--------|-------------------|
| Stakeholder | `Motivation_Stakeholder` | Роль индивида, команды или организации, заинтересованной в эффектах архитектуры |
| Driver | `Motivation_Driver` | Внешнее/внутреннее условие, побуждающее задать цели и изменения |
| Assessment | `Motivation_Assessment` | Результат анализа положения дел относительно driver |
| Goal | `Motivation_Goal` | Высокоуровневое намерение / желаемое конечное состояние |
| Outcome | `Motivation_Outcome` | Конечный результат / эффект некоторого положения дел |
| Principle | `Motivation_Principle` | Общее свойство, применимое к любой системе в контексте |
| Requirement | `Motivation_Requirement` | Потребность: свойство конкретной системы в архитектуре |
| Constraint | `Motivation_Constraint` | Ограничение на архитектуру, реализацию или процесс внедрения |
| Meaning | `Motivation_Meaning` | Знание/интерпретация концепта в контексте |
| Value | `Motivation_Value` | Относительная ценность / полезность концепта |
### 3.2. Strategy
| Элемент | Макрос | Определение (N221) |
|---------|--------|-------------------|
| Resource | `Strategy_Resource` | Актив, которым владеют или управляют |
| Capability | `Strategy_Capability` | Способность, которой обладает активная структура |
| Value Stream | `Strategy_ValueStream` | Последовательность активностей, создающая результат для клиента/стейкхолдера |
| Course of Action | `Strategy_CourseOfAction` | Подход/план настройки capabilities и resources для достижения цели |
### 3.3. Business
| Элемент | Макрос | Аспект | Определение (N221) |
|---------|--------|--------|-------------------|
| Business Actor | `Business_Actor` | Active | Сущность, способная выполнять поведение |
| Business Role | `Business_Role` | Active | Ответственность за поведение; роль актора |
| Business Collaboration | `Business_Collaboration` | Active | Агрегат ≥2 внутренних active structure для коллективного поведения |
| Business Interface | `Business_Interface` | Active | Точка доступа, где бизнес-сервис доступен среде |
| Business Process | `Business_Process` | Behavior | Последовательность бизнес-поведения к конкретному результату |
| Business Function | `Business_Function` | Behavior | Набор поведения по критериям (ресурсы/компетенции) |
| Business Interaction | `Business_Interaction` | Behavior | Коллективное поведение ≥2 акторов/ролей/коллабораций |
| Business Event | `Business_Event` | Behavior | Изменение состояния на бизнес-уровне |
| Business Service | `Business_Service` | Behavior | Явно определённое поведение, доступное среде |
| Business Object | `Business_Object` | Passive | Концепт предметной области |
| Contract | `Business_Contract` | Passive | Соглашение провайдер–потребитель (права/обязанности, параметры) |
| Representation | `Business_Representation` | Passive | Воспринимаемая форма информации бизнес-объекта |
| Product | `Business_Product` | Composite | Согласованный набор сервисов/пассивных элементов + contract |
### 3.4. Application
| Элемент | Макрос | Аспект | Определение (N221) |
|---------|--------|--------|-------------------|
| Application Component | `Application_Component` | Active | Инкапсуляция прикладной функциональности, модульная и заменяемая |
| Application Collaboration | `Application_Collaboration` | Active | Агрегат ≥2 application internal active structure |
| Application Interface | `Application_Interface` | Active | Точка доступа к application services |
| Application Function | `Application_Function` | Behavior | Автоматизированное поведение компонента |
| Application Interaction | `Application_Interaction` | Behavior | Коллективное поведение ≥2 компонентов |
| Application Process | `Application_Process` | Behavior | Последовательность прикладного поведения к результату |
| Application Event | `Application_Event` | Behavior | Изменение состояния приложения |
| Application Service | `Application_Service` | Behavior | Явно определённое внешнее прикладное поведение |
| Data Object | `Application_DataObject` | Passive | Данные, структурированные для обработки |
### 3.5. Technology (+ Physical в гл. Technology 3.2)
| Элемент | Макрос | Определение (N221) |
|---------|--------|-------------------|
| Node | `Technology_Node` | Вычислительный/физический ресурс, хостящий другие ресурсы |
| Device | `Technology_Device` | Физический ИТ-ресурс для хранения/исполнения ПО и артефактов |
| System Software | `Technology_SystemSoftware` | ПО среды исполнения/хранения |
| Technology Collaboration | `Technology_Collaboration` | Агрегат ≥2 technology internal active structure |
| Technology Interface | `Technology_Interface` | Точка доступа к technology services |
| Path | `Technology_Path` | Связь между ≥2 technology internal active structure для обмена |
| Communication Network | `Technology_CommunicationNetwork` | Структуры/поведение для передачи данных |
| Technology Function | `Technology_Function` | Набор технологического поведения |
| Technology Process | `Technology_Process` | Последовательность технологического поведения |
| Technology Interaction | `Technology_Interaction` | Коллективное technology-поведение |
| Technology Event | `Technology_Event` | Изменение технологического состояния |
| Technology Service | `Technology_Service` | Явно определённое внешнее technology-поведение |
| Artifact | `Technology_Artifact` | Данные/файл, используемый или порождаемый разработкой/эксплуатацией |
| Equipment | `Physical_Equipment` | Физические машины/инструменты для материи |
| Facility | `Physical_Facility` | Физическая структура или окружение |
| Distribution Network | `Physical_DistributionNetwork` | Физическая сеть транспортировки материи/энергии |
| Material | `Physical_Material` | Осязаемая материя или энергия |
В 3.2 Device / System Software / Facility / Equipment — **не** подтипы Node, а technology internal active structure с composition/aggregation к Node (changelog C226).
### 3.6. Implementation & Migration
| Элемент | Макрос | Определение (N221) |
|---------|--------|-------------------|
| Work Package | `Implementation_WorkPackage` | Серия действий с результатом в сроках и ресурсах |
| Deliverable | `Implementation_Deliverable` | Точно определённый результат work package |
| Implementation Event | `Implementation_Event` | Изменение состояния, связанное с внедрением/миграцией |
| Plateau | `Implementation_Plateau` | Относительно стабильное состояние архитектуры на период |
| Gap | `Implementation_Gap` | Различие между двумя plateaus |
### 3.7. Composite и Junction
| Элемент | Макрос | Определение (N221) |
|---------|--------|-------------------|
| Grouping | `Grouping` / `Other_Grouping` / `Group` | Агрегирует/композирует концепты по общему признаку |
| Location | `Other_Location` / `Business_Location` | Место, где расположены или выполняются концепты |
| And Junction | `Junction_And` | Логическое И для связей одного типа |
| Or Junction | `Junction_Or` | Логическое ИЛИ для связей одного типа |
Grouping и Junction допустимы **в любом** viewpoint (спецификация).
### 3.8. Когда какой элемент поведения
| Нужно | Берите |
|-------|--------|
| Последовательность шагов к результату | `*_Process` |
| Группа поведения «изнутри» по компетенциям/ресурсам | `*_Function` |
| Поведение коллаборации (≥2 участников) | `*_Interaction` |
| Внешне видимое поведение (чёрный ящик) | `*_Service` |
| Момент / смена состояния | `*_Event` |
**Не смешивайте слои:** `Business_Process` ≠ `Application_Process` ≠ `Technology_Process`. Слой задаётся типом элемента, не подписью.
---
## 4. Связи
### 4.1. Одиннадцать типов (N221) + PlantUML
| Тип | Класс | Смысл (N221) | Макрос | Нотация PlantUML |
|-----|-------|--------------|--------|------------------|
| Composition | Structural | Элемент **состоит из** других; часть не существует без целого | `Rel_Composition` | `*--` |
| Aggregation | Structural | Элемент **объединяет** другие; части могут жить отдельно | `Rel_Aggregation` | `o--` |
| Assignment | Structural | Ответственность / выполнение поведения / хранение / исполнение | `Rel_Assignment` | `@-->>` |
| Realization | Structural | Критическая роль в создании/достижении более абстрактного | `Rel_Realization` | `~~|>` |
| Serving | Dependency | Предоставляет функциональность другому | `Rel_Serving` | `-->` |
| Access | Dependency | Поведение/active structure наблюдает или действует на passive | `Rel_Access` / `_r` / `_w` / `_rw` | `~~` / `<-~` / `~->` / `<~~>` |
| Influence | Dependency | Влияет на реализацию/достижение motivation-элемента (`+/-`) | `Rel_Influence` | `..>` |
| Triggering | Dynamic | Временная/причинная активация | `Rel_Triggering` | `-->>` |
| Flow | Dynamic | Передача чего-либо | `Rel_Flow` | `..>>` |
| Specialization | Other | Является разновидностью | `Rel_Specialization` | `--|>` |
| Association | Other | Неспецифичная связь (или нет другого типа) | `Rel_Association` / `_dir` | `--` / `--\` |
| Junction | Connector | Соединяет связи **одного** типа | `Junction_And` / `Junction_Or` | circle |
Направление на схеме: `Rel_Type_Up` / `_Down` / `_Left` / `_Right` (например `Rel_Serving_Up(svc, proc)`).
### 4.2. Направления (частые ошибки)
| Связь | Откуда → куда |
|-------|----------------|
| **Serving** | Поставщик (часто Service) → **потребитель** |
| **Realization** | Более конкретное → более абстрактное |
| **Composition / Aggregation** | Целое → часть |
| **Assignment** | Active structure → behavior (или ресурс хранения → artifact) |
| **Access** | Behavior (или active) → **passive**; тип: read / write / read-write |
| **Triggering** | Предшественник → следующий |
| **Flow** | Источник передачи → приёмник |
| **Influence** | Влияющий → motivation-элемент (сила `++`/`+`/`0`/`-`/`--` в подписи) |
| **Specialization** | Специализация → общий тип |
На листах: realization — «вверх» по абстракции; serving — к тому, кого обслуживают. Access всегда к объекту, независимо от отрисовки стрелки.
### 4.3. Правила с листов (метамодель ядра)
- Любой элемент может **compose/aggregate** элементы **того же класса** (на обзорных диаграммах часто не рисуют).
- Внутренние поведения одного слоя могут compose/aggregate друг друга (Function агрегирует Process).
- **Association** формально возможна между любыми классами — но на схеме используйте её только когда нет более точного типа.
- Collaboration ведёт себя как соответствующий internal active structure.
- Stakeholder — Association с любым элементом; элементы мотивации (кроме Stakeholder) — Influence друг на друга.
### 4.4. Чего нет в корпусе
Полная **Appendix B** (матрица всех допустимых пар и derivation) в sample PDF **обрезана**. Ниже — практические цепочки из §3.3, N221, листов и типичных viewpoint’ов. Не выдавайте их за полную нормативную матрицу B.
---
## 5. Что с чем связывать
### 5.1. Канонические цепочки
```text
# Внутри слоя
Business_Actor/Role Rel_Assignment Business_Process/Function
Business_Process Rel_Realization Business_Service
Business_Process Rel_Access_* Business_Object
Business_Event Rel_Triggering Business_Process
Application_Component Rel_Assignment Application_Function/Process
Application_Function Rel_Realization Application_Service
Application_Service Rel_Serving Business_Process/Role
Application_Component Rel_Access_* Application_DataObject
Application_Component Rel_Composition Application_Component
Technology_Node Rel_Composition Technology_Device / SystemSoftware
Technology_* Rel_Assignment Technology_Function/Process
Technology_Function Rel_Realization Technology_Service
Technology_Service Rel_Serving Application_Component
Technology_Artifact Rel_Realization Application_DataObject
Application_DataObject Rel_Realization Business_Object
# Стратегия / мотивация
Business_Process / Application_Service Rel_Realization Strategy_Capability
Application_Service / Component Rel_Realization Motivation_Requirement
Motivation_Driver Rel_Association Motivation_Stakeholder
Motivation_Assessment Rel_Association Motivation_Driver
Motivation_Goal Rel_Realization Motivation_Outcome
Motivation_Requirement Rel_Specialization Motivation_Constraint # или наоборот по смыслу «ограничение как вид требования» — лучше Constraint отдельно + Influence/Association
# Миграция
Implementation_WorkPackage Rel_Realization / Aggregation Implementation_Deliverable
Implementation_Plateau Rel_Aggregation core elements
Implementation_Gap Rel_Association between plateaus
```
### 5.2. Запреты для генерации
1. Не `Rel_Assignment` от Data Object / Business Object к процессу.
2. Не `Rel_Realization` от Component сразу к Process — нужен Function/Process того же слоя, затем Realization к Service.
3. Не `Rel_Serving` «потребитель → сервис» (перевёрнуто).
4. Не подменять Access потоком Flow «потому что данные».
5. Не рисовать `Business_Process` вместо `Application_Process`, если исполнитель — компонент.
6. Не ставить Association на все рёбра «на всякий случай».
7. Не считать Device подтипом Node без composition/aggregation (3.2).
---
## 6. Viewpoints
**Один рисунок = один viewpoint.** Не сваливайте мотивацию, бизнес, приложения и ЦОД на один холст без нужды (Layered — исключение, но всё равно ограничивайте число элементов).
Имена — из оглавления sample C226, Appendix C:
### 6.1. Basic (C.1)
| Viewpoint | Фокус | Типичные элементы |
|-----------|-------|-------------------|
| Organization | Структура орг. единиц | Actor, Role, Collaboration, Location, Interface |
| Application Structure | Внутренняя структура приложений | Component, Collaboration, Interface, Data Object |
| Information Structure | Данные/информация | Business Object, Representation, Data Object, Artifact, Meaning |
| Technology | Инфраструктура | Node, Device, System Software, Network, Path, Artifact… |
| Layered | Обзор слоёв | Набор из Business + Application + Technology + serving/realization |
| Physical | Физический мир | Equipment, Facility, Distribution Network, Material + tech |
| Product | Ценность продукта | Product, Contract, Services, Interfaces |
| Application Usage | Приложения в бизнес-контексте | Process/Function/Service бизнес + application |
| Technology Usage | Технологии для приложений | Application + Technology services/nodes |
| Business Process Cooperation | Связь процессов | Processes, Events, Services, Roles, supporting apps |
| Application Cooperation | Связь приложений | Components, Services, Interfaces, Data, Flow/Serving |
| Service Realization | Как сервисы реализованы | Service ← Process/Function ← Active structure |
| Implementation and Deployment | Развёртывание | Application + Artifact + Node/Device |
### 6.2. Motivation (C.2)
Stakeholder · Goal Realization · Requirements Realization · Motivation — Driver/Assessment/Goal/Outcome/Principle/Requirement/Constraint/Value/Meaning и Realization/Influence к core.
### 6.3. Strategy (C.3)
Strategy · Capability Map · Value Stream · Outcome Realization · Resource Map.
### 6.4. Implementation & Migration (C.4)
Project · Migration · Implementation and Migration — Work Package, Deliverable, Plateau, Gap, Event.
### 6.5. Макет
- Организация / структура: `LAYOUT_TOP_DOWN()` или вложенность `{ }`.
- Процессы слева направо: `LAYOUT_LEFT_RIGHT()` + `Rel_*_Right`.
- Layered: бизнес сверху, technology снизу; serving вверх (`Rel_Serving_Up`), realization вверх к абстракции.
- Широкие схемы: `%id … +landscape`.
Пример композиции (van den Berg): сначала **Motivation viewpoint** (риски/цели), затем отдельные схемы процессов с **Junction** на ветвлениях восстановления.
---
## 7. Синтаксис PlantUML
Документация: [plantuml.com/archimate-diagram](https://plantuml.com/archimate-diagram). Stdlib: Archimate-PlantUML **3.2.1**.
### 7.1. Рекомендуемый путь (схема md2gost)
```text
LAYOUT_TOP_DOWN() ' или LAYOUT_LEFT_RIGHT()
Business_Actor(a, "Имя")
Application_Service(s, "Сервис")
Rel_Serving_Up(s, a)
```
Include и `@startuml` добавляет схема `archimate`.
### 7.2. Низкоуровневый keyword (без макросов)
Только в `` ```uml ``, если stdlib недоступен:
```plantuml
@startuml
archimate #Business "Подача заявки" as p <<business-process>>
archimate #Application "Портал" as c <<application-component>>
c --> p
@enduml
```
Sprites: `<<$archimate/business-process>>` или `jar:archimate/...`.
**Не смешивайте** макросы и сырой `archimate` на одной схеме.
### 7.3. Темы
```plantuml
!theme archimate-standard from <archimate/themes>
' archimate-alternate | saturated | lowsaturation | handwriting
```
### 7.4. Nesting и special shapes
```plantuml
Business_Product(prod, "Тариф") {
Business_Service(s1, "Подключение")
Business_Contract(c1, "Оферта")
}
```
Для special shapes (`$ARCH_SPECIAL_SHAPES`) у Service/Actor/Value/ValueStream при вложении: `$nest=%true()`.
### 7.5. Layout helpers
`Lay_U` / `Lay_D` / `Lay_L` / `Lay_R` — скрытые связи для выравнивания.
`LAYOUT_AS_SKETCH()` — черновик (не для финального ГОСТ-рисунка).
### 7.6. Sequence с ArchiMate
Только если явно просят sequence: `!global $ARCH_SEQUENCE_SUPPORT = %true()` до include. Это **не** замена viewpoint.
### 7.7. List sprites
```plantuml
@startuml
listsprite
@enduml
```
---
## 8. Чеклист для ИИ
1. Версия языка: **ArchiMate 3.2**; макросы слоя (`Business_*`, `Application_*`, …).
2. Оградка `` ```uml-archimate ``; без `@startuml` / `!include`.
3. Выбрать **один** viewpoint; 8–18 элементов на рисунок.
4. Сначала все элементы, потом все `Rel_*`.
5. Alias латиницей; подписи по-русски в кавычках; длинные имена с `\n`.
6. Serving от сервиса к потребителю; Realization от конкретного к абстрактному.
7. Поведение того же слоя, что и исполнитель (Assignment).
8. Не UML class/component, не C4 `Person`, не BPMN `XOR`, не mxgraph.
9. Не Association «по умолчанию».
10. Для отчёта: `%id Название` (+ `+landscape` если широко).
---
## 9. Готовые шаблоны
Тела для `` ```uml-archimate `` (без обёртки).
### 9.1. Organization
```plantuml
LAYOUT_TOP_DOWN()
Business_Actor(uni, "Университет") {
Business_Actor(dean, "Деканат")
Business_Actor(it, "Управление ИТ")
}
Business_Role(student, "Студент")
Business_Role(clerk, "Методист")
Rel_Assignment(dean, clerk)
Rel_Assignment(uni, student)
```
### 9.2. Service Realization
```plantuml
LAYOUT_TOP_DOWN()
Business_Role(applicant, "Абитуриент")
Business_Process(apply, "Подача заявления")
Business_Service(applySvc, "Приём заявлений")
Application_Service(webSvc, "Веб-приёмная")
Application_Component(lk, "Личный кабинет")
Application_Function(submitFn, "Отправка формы")
Rel_Assignment(applicant, apply)
Rel_Realization(apply, applySvc)
Rel_Serving(webSvc, apply)
Rel_Assignment(lk, submitFn)
Rel_Realization(submitFn, webSvc)
```
### 9.3. Application Cooperation
```plantuml
LAYOUT_LEFT_RIGHT()
Application_Component(crm, "CRM")
Application_Component(billing, "Биллинг")
Application_Service(crmApi, "API клиентов")
Application_DataObject(client, "Карточка клиента")
Rel_Realization(crm, crmApi)
Rel_Serving(crmApi, billing)
Rel_Access_rw(crm, client)
Rel_Access_r(billing, client)
Rel_Flow(crm, billing, "событие оплаты")
```
### 9.4. Layered
```plantuml
LAYOUT_TOP_DOWN()
Business_Process(sale, "Оформление заказа")
Business_Object(order, "Заказ")
Application_Service(shopSvc, "Интернет-магазин")
Application_Component(shop, "ShopApp")
Technology_Service(dbSvc, "СУБД")
Technology_Node(dbHost, "DB Host")
Technology_SystemSoftware(pg, "PostgreSQL")
Rel_Access_w(sale, order)
Rel_Serving_Up(shopSvc, sale)
Rel_Realization(shop, shopSvc)
Rel_Serving_Up(dbSvc, shop)
Rel_Assignment(dbHost, dbSvc)
Rel_Composition(dbHost, pg)
```
### 9.5. Motivation / Requirements Realization
```plantuml
LAYOUT_TOP_DOWN()
Motivation_Stakeholder(rector, "Ректор")
Motivation_Driver(comp, "Конкуренция вузов")
Motivation_Goal(digital, "Цифровизация приёма")
Motivation_Requirement(online, "Заявление онлайн 24/7")
Motivation_Constraint(152fz, "152-ФЗ")
Application_Service(portal, "Портал абитуриента")
Rel_Association(rector, comp)
Rel_Association(comp, digital)
Rel_Realization(online, digital)
Rel_Influence(152fz, online, "-")
Rel_Realization(portal, online)
```
### 9.6. Implementation & Migration
```plantuml
LAYOUT_LEFT_RIGHT()
Implementation_Plateau(asIs, "AS-IS 2025")
Implementation_Plateau(toBe, "TO-BE 2026")
Implementation_Gap(gap1, "Нет единого ЛК")
Implementation_WorkPackage(wp, "Проект Единый ЛК")
Implementation_Deliverable(d1, "Релиз 1.0")
Implementation_Event(goLive, "Go-Live")
Rel_Association(gap1, asIs)
Rel_Association(gap1, toBe)
Rel_Realization(wp, d1)
Rel_Triggering(d1, goLive)
Rel_Triggering(goLive, toBe)
```
### 9.7. Technology Usage
```plantuml
LAYOUT_TOP_DOWN()
Application_Component(api, "API Gateway")
Technology_Node(k8s, "Kubernetes")
Technology_Device(node1, "Worker x86")
Technology_SystemSoftware(os, "Linux")
Technology_CommunicationNetwork(net, "Cluster CNI")
Technology_Artifact(image, "api:1.2.img")
Rel_Serving_Up(k8s, api)
Rel_Composition(k8s, node1)
Rel_Composition(node1, os)
Rel_Association(node1, net)
Rel_Assignment(node1, image)
```
### 9.8. Process с Junction (в духе van den Berg)
```plantuml
LAYOUT_LEFT_RIGHT()
Business_Event(incident, "Инцидент")
Business_Process(triage, "Триаж")
Junction_Or(j1, "")
Business_Process(restore, "Восстановление КФ")
Business_Process(investigate, "Расследование")
Business_Process(close, "Закрытие")
Rel_Triggering(incident, triage)
Rel_Triggering(triage, j1)
Rel_Triggering(j1, restore)
Rel_Triggering(j1, investigate)
Rel_Triggering(restore, close)
Rel_Triggering(investigate, close)
```
---
## 10. Антипаттерны
| Плохо | Почему | Как надо |
|-------|--------|----------|
| Все связи `Rel_Association` | Теряется семантика | Serving / Realization / Assignment / Access |
| `Application_Component` вместо `Business_Actor` | Разные аспекты и слои | Actor/Role для людей/орг. единиц |
| Business Process «внутри» компонента без Assignment | Слои смешаны | Process на бизнес-слое; Serving от Application Service |
| Serving от Role к Service | Направление наоборот | Service → Role/Process |
| Realization Component → Process | Пропущен внутренний behavior | Component → Function → Service |
| Flow вместо Access к Data Object | Flow — передача, Access — работа с объектом | `Rel_Access_r/w/rw` |
| Один Layered на 40+ элементов | Нечитаемо | Несколько viewpoint’ов |
| C4 `Person` / UML class в archimate-блоке | Другой язык | Только макросы ArchiMate |
| Generic «Process» без слоя | В 3.2 слоя обязательны | `Business_Process` / `Application_Process` / … |
---
## 11. Источники и границы корпуса
### Норматив (при конфликте — сверху вниз)
1. **n221p.pdf** — ArchiMate 3.2 Reference Cards (N221): определения элементов и связей.
2. **978940180955C_SMPL.pdf** (+ дубль SMPL-1) — sample C226: гл. 1–2, начало гл. 3, оглавление (в т.ч. список viewpoints Appendix C).
3. **archimate-sheets-ru-20230805-s.pdf** — листы метамодели 3.2 (Виерда / Ефремов): прямые связи, цвета аспектов на листах. Описания элементов на листах **не** от Open Group — для формулировок «что такое X» побеждает N221.
### Синтаксис рисунка
- [plantuml.com/archimate-diagram](https://plantuml.com/archimate-diagram)
- Stdlib Archimate-PlantUML 3.2.1 (`!include <archimate/Archimate>`)
### Примеры композиции (не словарь языка)
- hospital ZiRA preview (inkijkexemplaar) — reference architecture / C226.
- **vandenBerg_MA_EEMCS.pdf** — §2.4 framework, motivational viewpoint, Junction в процессах BCM/DR.
### Не правила ArchiMate
- Article_37 (FEM), paper_41 (OntoUML/COVO), FASD (UML атак), 419 (единое окно), **2659430** (Packt Practical Cybersecurity Architecture — процесс кибер-архитектуры, не спецификация 3.2).
### Ограничения
- Полная Appendix B в sample отсутствует — матрица пар здесь практическая, не нормативная таблица B.5.
- PDF спецификации в git не хранятся.
- Старый PlantUML/Kroki без stdlib 3.2: fallback — `` ```uml `` + keyword `archimate`.
- Пользовательский `md2gost.schemes.json`: встроенные схемы с `author: md2gost` синхронизируются с бандлом; свои правки (другой author) не затираются.
---
См. также: [schemes.md](schemes.md), меню **Шаблоны UML**, **Справка → Промпт для ИИ** (схема `archimate`).
+243
View File
@@ -0,0 +1,243 @@
# Отчёт по аудиту и оптимизации
Проведён 9-10 сентября 2026. Незакрытые задачи — в [TODO.md](TODO.md), команды ручной чистки git — в [git-cleanup.md](git-cleanup.md).
**Ничего не закоммичено.** Всё лежит в staging и рабочем дереве под ревью.
## Итог одной таблицей
| | Было | Стало |
|---|---|---|
| Замер страницы из 5 абзацев | 80.06 мс | 18.08 мс |
| Конструирований `Font` за проход | 19 | 0 |
| Вызовов `get_text_width` за проход | 814 | 114 |
| Конвертация формулы | 13.29 мс | 0.66 мс |
| PlantUML, 4 диаграммы, PNG | 4.99 с | 1.25 с |
| PlantUML, 4 диаграммы, SVG | 9.02 с | 2.33 с |
| Запусков JVM на документ | N (2N в SVG) | 1 (2 в SVG) |
| Сквозная конвертация документа | — | на 10-23% быстрее |
| Тесты | 54 passed | 320 passed, 174 subtests |
Объём изменений: 34 файла правок (+1552 / −698), плюс 34 файла удалений (−8095 строк).
---
## 1. Исправлено три бага вывода
Работа шла строго по TDD: сначала три падающих теста, прогон с 9 падениями по ожидаемым причинам, затем правки. Каждый фикс привязан к пункту методички МИРЭА (`docs/reference/method-mirea-2022.txt`).
### Гиперссылка дублировала свой текст
`md2gost/renderable/paragraph.py`, строка 44. В `Link.add_run` цикл по сегментам присваивал `docx_run.text = text` вместо `part`, поэтому ссылка с дефисом выводилась полным текстом столько раз, сколько в ней сегментов.
Красный тест показал это нагляднее всего на интервале: `['100', '150'] != ['100-150', '100-150']`. На сложном слове: `'вопервых' != 'во-первыхво-первых'`.
Опечатка при копировании из `Paragraph.add_run`, где на строке 104 тот же цикл написан правильно.
Обоснование: строки 373-376 методички — дефис не отделяется пробелами и используется в сложных словах («во-первых», «физико-математический») и интервалах («100-150 Мб»), поэтому слово не должно рваться при переносе. Ради этого и вставляется отдельный элемент `w:noBreakHyphen` вместо текстового дефиса.
### Курсив в подсветке синтаксиса не доезжал до XML
`md2gost/renderable/listing.py`, строка 36. Выражение `style["italic"] in style or None` проверяло, есть ли значение `True`/`False` среди **ключей** словаря стиля. Всегда ложно, курсив не применялся никогда.
Здесь пришлось разбираться, что вообще считать правильным поведением. Табл. 4.1 методички (строки 716-721) требует для содержимого листинга `Courier New / Начертание Обычный / 10 пт / Цвет Авто (чёрный)`. Стили в `style_config.py:303-314` этому соответствуют, но `DocxParagraphPygmentsFormatter` перекрывает их на уровне run'а, навешивая полужирный и цвет. То есть подсветка как таковая противоречит методичке, а сломанный курсив лишь случайно оказывался ближе к требованию.
Решение в два шага: выражение исправлено на `style.get("italic") or None`, чтобы флаг делал то, что обещает, и добавлено правило `listing.syntax_highlighting` в checker — предупреждение по умолчанию, ошибка под `--strict`. Флаг выключен по умолчанию, так что это осознанное отклонение, но пользователь узнаёт о нём из отчёта, а не из замечаний преподавателя.
Заодно добавлен регрессионный страж: при выключенном флаге ни один run в стиле `Code` не должен задавать `w:b`, `w:i` или `w:color`.
### Пустой заголовок ронял парсер
`md2gost/extended_markdown/heading.py`, строка 23. `inline_body[0] == "*"` давал `IndexError` на строке `#` без текста. Заменено на `startswith`.
Плюс правило `heading.empty` в checker'е: пустой заголовок дал бы пронумерованный безымянный раздел в автособираемом содержании. Обоснование — строка 156 методички: «разделы, подразделы, пункты имеют заголовки, четко и кратко отражающие их содержание».
Побочно выяснилось расхождение в постановке: я просил проверить форму `#*`, но такая строка регуляркой заголовка вообще не матчится — после `#` обязателен пробел или конец строки. Заголовком является только `# *`, эта форма и покрыта тестом.
---
## 2. Горячий путь измерения текста ускорен в 4.4 раза
### Кэш объектов `Font`
Замер показал, что один `Font()` конструируется 2.62 мс: открывает FreeType `Face`, грузит `ImageFont.truetype` и аллоцирует PIL-канвас 1000×1000. При этом он создавался дважды за вызов `calculate_height` плюс по разу на каждый run.
Добавлен модульный `_get_font(name, bold, italic, size_pt)` под `lru_cache(maxsize=256)`, все три места конструирования переведены на него. Сам класс `Font` оставлен публичным — существующие тесты создают его напрямую.
### Скрытая мина в `is_mono`
Попутно нашлось то, чего в плане не было. Ветка `get_text_width` для моноширинных шрифтов читала `self._face.glyph.advance.x` — то есть тот глиф, который последним оставил в слоте `load_char`. Всегда оказывался `"m"`, потому что единственные вызовы `load_char` живут в `is_mono`. Значение было верным, но верным по случайности: любое постороннее обращение к face молча его сломало бы.
`is_mono` заменён на свойство `_metrics`, которое снимает и флаг моноширинности, и advance за один проход. Результат теперь не зависит от порядка обращений к слоту глифа. Есть тест, который долбит `load_char` посторонними символами и проверяет, что ширина не поехала.
### Квадратичный пересчёт ширины слова
`count_lines` пересчитывал ширину префикса слова после каждого символа, хотя результат читается только в ветке `if c == " "`. Вызов перенесён в две точки: на пробеле и один раз в конце каждого run.
Вторая точка — та, которую легко пропустить: слово может пересекать границу run'ов, а следующая итерация добавляет свежий слот в `word_parts_widths`, поэтому фрагмент нужно померить до этого.
Намеренно сохранено одно неочевидное поведение оригинала: `word_part` не сбрасывается после замера на пробеле, потому что исходный код сбрасывал его только внутри ветки `if any(word_parts_widths)`, и фрагмент нулевой ширины продолжал накапливаться через пробел.
### Синглтон XSLT
`md2gost/latex_math.py` парсил `mml2omml` с диска и компилировал `etree.XSLT` заново на каждую формулу. Вынесено в ленивый `_mml2omml_transform()`. Оставлено внутри `try`, чтобы битый или отсутствующий stylesheet по-прежнему давал тот же `ValueError` при каждом вызове.
### Как доказана эквивалентность
Не чтением кода. Пять эталонных тестовых файлов прогнаны до и после, verbose-вывод сдиффен построчно — идентичен. Плюс `tests/test_paragraph_sizer_cache.py` содержит дословные копии дооптимизационных `Font` и `count_lines` и сверяет с ними новый код на 158 случаях: одиночные и многорановые абзацы, разрыв слова на границе run'а, пустые run'ы, `w:noBreakHyphen`, смешанные полужирный с курсивом, кириллица, длинные неразрывные слова, сдвоенные и хвостовые пробелы, три ширины колонки, оба значения `is_mono`.
Чтобы убедиться, что корпус не вырожденный, замер в конце run'а был временно отключён — тесты упали в 5 местах.
---
## 3. Диаграммы: одна JVM на документ вместо одной на диаграмму
Перед проектированием было проверено фактическое поведение jar: PlantUML принимает много входных файлов за запуск, но только один флаг `-t<fmt>` (передача `-tpng -tsvg` молча отдаёт только SVG), а относительный `-o` резолвится от каталога каждого входного файла.
Отсюда конструкция: каждый источник получает свой подкаталог `NNNN/src.puml` с `-o out`. Это заодно решает коллизию двух источников, объявляющих `@startuml SameName`.
`DiagramFigure.__init__` регистрирует свой блок через `register_pending_diagram()`. Поскольку `Converter.convert()` полностью материализует `list(self.parser.parse())` до рендера, к моменту первого вызова `render_diagram()` очередь содержит весь документ. Первый вызов запускает одноразовый предпроход: готовит все источники, отбрасывает попавшие в кэш, остальные рендерит одной JVM.
Всё, что упало в предпроходе, остаётся промахом кэша и идёт обычным путём, так что цепочка PlantUML jar → локальный Kroki → удалённый Kroki и режимы `--diagram-fallback` не тронуты. `_render_plantuml_jar_one` теперь обёртка на одну задачу поверх того же батч-раннера, чтобы одиночный и пакетный пути не разъехались.
### Найденный попутно баг
Старый код писал `diagram.puml` и искал `diagram.{fmt}`. Любой источник с `@startuml SomeName` производил `SomeName.png`, считался провалом jar и молча уезжал на Kroki. Новая логика забирает то, что jar реально положил в выходной каталог этой диаграммы, и рендерит локально.
### Остальное по диаграммам
- Версия jar добавлена в ключ кэша: `PLANTUML_CACHE_PREFIX = f"plantuml-{PLANTUML_VERSION}\n"`. Раньше для plantuml префикс был пустой, и обновление jar отдавало старые PNG.
- `ensure_user_schemes()` убран из `render_diagram` — он мигрировал `md2gost.schemes.json` на каждую диаграмму. Безопасно: `load_merged_schemes()` его и так вызывает.
- Добавлен `shutdown_diagrams()`, закрывающий сессию Playwright, вызывается из существующего `finally` в `pipeline.convert()`.
- Graphviz для DFD теперь отдаёт PNG и SVG из одного процесса `dot`. Проверяет наличие и непустоту каждого выхода и повторяет недостающий формат старым путём, если сборка Graphviz не поддерживает пары.
Что проверено вживую: путь через jar целиком, включая батч, попадания в кэш, источники с `@startuml Name`, полный прогон `pipeline.convert()`. Graphviz тоже оказался установлен — комбинированный запуск реально произвёл оба файла, и на этом всплыло, что существующий DFD-тест тянулся мимо своего мока.
Что не проверено: локальный и удалённый Kroki (сервера нет, сетевых вызовов не делалось) — только моки; закрытие браузера в реальной mermaid-конвертации, потому что здешние тесты идут через QuickJS.
---
## 4. Двойная работа при рендере убрана
Все три места сначала обвешаны замерами, потом правлены. Доказательство для каждого — матрица из 4 документов на 7 наборов опций, 28 конвертаций, и побайтовое сравнение `word/document.xml` со снимком, снятым до любых правок. **28 из 28 идентичны.** В матрице `examples/example.md`, `examples/pis_custom.md`, синтетический стресс-документ (многостраничная дробящаяся таблица со слитыми ячейками, многостраничный листинг, три фигуры) и документ с альбомной ориентацией, каждый в режимах продолжений off / legacy / caption / soft, плюс ручное содержание, тип `PIS_custom` и авто-нумерация заголовков.
### Повторный рендер на границе страницы
`md2gost/renderer.py`. Раньше: отрендерить блок, померить первый `RenderedInfo`, и если не влезает — выбросить генератор и вызвать `render()` заново. Для таблицы или листинга это пересборка всего поддерева DOCX и повтор всех замеров.
Решение оказалось точнее, чем «разделить measure и emit», и это выяснилось именно замером. Повтор нужен только когда в `self._to_new_page` реально что-то стоит в очереди: отложенные фигуры вставляются первыми и двигают границу страницы, поэтому блок приходится раскладывать заново уже против оставленного ими состояния. Если очередь пуста, `_flush_to_new_screen()` — no-op, и повторный рендер строит то же поддерево против того же состояния.
Инструментирование показало, что **во всех срабатываниях на всей матрице очередь была пуста**, то есть второй проход был чистой потерей всегда. Триггером почти везде оказывались заголовки с `page_break_before` и одна фигура — ни разу таблица или листинг.
Условие дополнено проверкой `and self._to_new_page`. Структура render → measure → flush → re-render сохранена ровно для того случая, когда flush действительно меняет состояние.
### Подпись рисунка мерилась дважды
`md2gost/renderable/image.py`. Предложенный в плане `Image.estimate_height()` **не понадобился**: замер показал, что геометрия считается дёшево, а реальная стоимость сухого прохода — построение свежего `Caption` и прогон сайзера по нему. Поэтому подпись создаётся один раз и переиспользуется (сбрасывается в `set_number()`), а сама структура «сухой проход плюс настоящий рендер» не тронута. Именно поэтому вывод и смог остаться идентичным.
### Кэш замеров абзаца
Строки таблицы мерились до трёх раз. Теперь `_peek_row_height` и `_build_docx_row` идут через один мемоизированный замер, а выбрасываемый сухой `Caption` в `_start_block_height` убран — `render()` строит подпись один раз и передаёт её внутрь.
Общий хелпер `measure_paragraph()` в `paragraph_sizer.py` мемоизирует `calculate_height()` в разрезе владеющего renderable. Ключ собран так, чтобы это было безопасно в коде, решающем разрывы страниц:
- сериализованный XML измеряемого абзаца, а **не** идентичность объекта — правка между двумя вызовами тогда даёт промах кэша вместо устаревшей высоты;
- сериализованный `w:pPr` предшественника, а не он целиком — сайзер читает только `pPr`, а привязка `w:p` к телу документа заставляет lxml поднять на него объявления пространств имён, из-за чего полная сериализация выглядела бы как правка и молча промахивалась;
- ширина.
Сверх побайтового сравнения был прогнан «паранойяльный» режим, пересчитывающий значение на каждом попадании в кэш и сверяющий с сохранённым. Вся матрица прошла без единого устаревшего попадания. Строительные леса убраны, а `tests/test_paragraph_sizer_cache.py` получил семь регрессионных тестов: попадание равно свежему расчёту, инвалидация при правке текста и при правке интервалов, ширина и интервалы предшественника как части ключа, контракт приватной копии (вызывающий обнуляет `.before` на новой странице), случай с привязкой предшественника.
Попутно `page_break_before = False` перенесён из конца `render()` в `__init__`, чтобы оба замера подписи видели один и тот же абзац.
**Сквозное время конвертации упало на 10-23%** в зависимости от документа.
---
## 5. Удалено 6777 строк мёртвого кода
Больше моей исходной оценки в 5646. Для каждого файла искались импорты модуля, все top-level символы (извлечены через `ast`) и имя файла в `*.spec`, `*.bat`, `*.toml`, `.github/workflows/`, `tests/` и всех живых пакетах.
| Файл | Строк | Чем был |
|---|---|---|
| `mm3.py` | 2210 | Монолит + matplotlib-графики |
| `mm2.py` | 1551 | Монолитный сборщик DOCX, версия 2 |
| `main12.py` | 915 | Ранний `MarkdownGostDocument` |
| `font.py` | 860 | Своя реализация Font API |
| `parser.py` | 340 | Песочница на mistune |
| `asd.py` | 317 | Эксперимент со стилями ГОСТ |
| `mm5.py` | 284 | Прототип на mistune |
| `mm4.py` | 241 | Построчный прототип |
| `main.py` | 32 | Hello-world на python-docx |
| `_audit_tmp.py` | 27 | Разовый инспектор DOCX |
Проверены две ловушки: корневой `parser.py` мёртв, а `md2gost/parser_.py` живой и импортируется в `converter.py`; корневой `font.py` мёртв, а `md2gost/renderable/find_font.py` живой. Единственное вхождение `font` в импортах живых пакетов — `from docx.text.font import Font as DocxFont`, то есть сторонняя библиотека.
Мёртвый код внутри пакета: убраны `APPENDIX_RE` в `renderer.py` (дублировал константу из `label_pass.py`), `Listing._create_table`, `Converter._output_path`, no-op ветка `isinstance(r, Paragraph): pass` в `label_pass.py`, `_BrowserSession.render_svg()`, `_draw_purpose()` в `idef0.py`, плюс осиротевший `import os` в `paragraph_sizer.py`.
Две поправки к моему списку. **`gui.main()` оказался живым** — это консольный скрипт `md2gost-gui` в `pyproject.toml`, я ошибочно записал его в мёртвые; оставлен как есть. И комментарий у `Listing._create_table`, утверждавший, что метод держат ради тестов, не подтвердился: тесты ссылаются на модульную функцию `create_table`, а не на метод.
У `Converter._output_path` заодно убран и параметр конструктора — оставлять параметр, который класс игнорирует, смысла нет. `pipeline.py` был единственным вызывающим, его вызов переведён на именованные аргументы, чтобы остальные позиционные не съехали.
---
## 6. Гигиена репозитория
Снято с отслеживания, файлы остались на диске: `ReadMe.docx`, `md2gost/README.docx`, `tmp_*.docx`, `README.md1`, `examples/example_out.docx`, `examples/toc_*.docx`, `tests/_idef0_preview*`, `latex/mirea/Settings/FiraCode-Regular.otf` (220 КБ, ни одной ссылки в `.tex`), каталог `ad/`, `md2gost.user.json`.
Перенесено с сохранением истории: `_method_extract.txt` → `docs/reference/method-mirea-2022.txt`, `tests/extended_mardown/` → `tests/extended_markdown/`.
`md2gost.user.json` содержал реальные ФИО и группу. Убран из индекса, файл на диске остался рабочим, рядом появился `md2gost.user.json.example` с пустыми полями — структура сгенерирована из `UserProfile()` и `DocumentMetadata()`, так что совпадает с тем, что пишет `save_user_profile`.
### Зависимости
Легаси-секции `[tool.poetry]`, `poetry.lock` и `requirements.txt` удалены, канон — `[project]` плюс `uv.lock`.
При этом выяснилось, что `matplotlib` был объявлен **только** в удаляемой Poetry-секции, хотя импортируется в рантайме из `find_font.py` на всех платформах кроме Linux. Добавлено `matplotlib>=3.7; sys_platform != 'linux'` в `[project].dependencies`. Также поправлен `pywin32` — он был добавлен без маркера платформы, что ломало бы `pip install .` на Linux-CI, теперь `pywin32; sys_platform == 'win32'`.
Поле `readme` исправлено с `README.md` на фактическое `ReadMe.md` — расхождение регистра сломало бы сборку колеса на Linux.
### CI
`example-generator.yml` не запускался ни разу: триггер стоял на ветку `main`, а репозиторий на `master`. Исправлено в обоих триггерах. Артефакт грузился из `example.docx`, тогда как `default_output_path` кладёт файл рядом с входным — путь исправлен на `examples/example.docx`. Имя воркфлоу совпадало с `pytest.yml`, переименовано в `Example document`. Обновлены `upload-artifact@v3` → `v4`, `checkout@v3` → `v4`, `setup-python@v3` → `v5`, добавлен `apt-get update` перед установкой шрифтов.
### Тесты
Два метода с именем `test_count_lines_courier7` затирали друг друга — второй переименован, расшадоуенный первый **проходит**, латентного бага не вскрылось. Голые `return` в двух win32-тестах `test_pipeline.py` заменены на `@pytest.mark.skipif` (раньше они проходили без единой проверки). Опечатка в имени каталога `extended_mardown` исправлена.
Кстати про нумерацию: `test_count_lines_courier7` — это индекс 7 в серии от `courier1` до `courier10`, а не размер 7 пт. Третьего размера шрифта не существует, есть только 10 в `style_config` и 12 в фикстуре и калибровке.
---
## 7. Дедупликация сиблинг-конвертеров
`md2latex/emitter.py` и `md2fodt/emitter.py` содержали примерно 150-200 строк почти идентичного обхода AST: диспетчер блоков, инлайн-обходчик, детектор строк библиографии, машина состояний `pending_caption`, словарь русских подписей ссылок.
Создан отдельный верхнеуровневый пакет `mdexport` — внутрь `md2gost` его класть было нельзя, а размещение в одном из экспортёров сделало бы один бэкенд зависимым от другого. Три модуля:
- `blocks.py` — `walk_blocks`, `walk_paragraph`, ABC `BlockVisitor`, кортеж отложенной подписи и `take_caption()`, детектор строк библиографии, парсинг заголовка картинки `%id Подпись`, `is_contents_heading`
- `inline.py` — `render_inline`, ABC `InlineRenderer`, таблица русских названий ссылок
- `labels.py` — `sanitize_label`, параметризованный разделителем: `:` для LaTeX, `-` для XML `NCName`
`escape_text` остался в бэкендах — там разные задачи, карта символов LaTeX против `saxutils` для XML.
Эмиттеры сократились на 119 строк, сам общий пакет занял 309, из которых около 120 — новый интерфейсный каркас: объявления абстрактных методов, докстринги, реэкспорты. По суммарному LOC выигрыш скромный, реальный смысл в том, что перечень типов узлов и порядок элементов теперь существуют в одном месте. Раньше новый тип узла приходилось добавлять в две цепочки `if/elif`, которые уже разъехались: в `md2latex` жил мёртвый словарь `cat`, которого на стороне FODT не было вовсе.
Побайтовая сверка: шесть снимков (`example.tex`, `example.fodt`, `example_emdash.fodt`, `pis_custom.tex`, `pis_custom.fodt`, `pis_custom_emdash.fodt`) совпали по SHA-256. Но образцы не дотягиваются до курсива, полужирного, кодовых вставок, ссылок, разрывов строк, цитат, вложенных списков, отсутствующих файлов картинок и экранирования специальных символов LaTeX — поэтому дооптимизационные эмиттеры были дополнительно выкачены из `HEAD` в temp и прогнаны на стресс-документе, покрывающем эти пути. `stress.tex`, `stress.fodt`, `stress_emdash.fodt` тоже идентичны.
`word2md/walker.py`: функция `_handle_paragraph` на 207 строк превращена в тело из 27 строк — собирается неизменяемая запись `_Para` с четырьмя фактами, нужными всем ветвям, затем прогоняется кортеж из одиннадцати обработчиков до первого, вернувшего не `None`. Возврат `None` означает «не мой случай, идём дальше», что сохранило два исходных сквозных пути: подпись со стилем подписи, чей текст не парсится, и строку со стилем библиографии без маркера `[n]`.
Тем же способом проверено и здесь: восемь конвертаций (синтетический DOCX с титульной частью, стилем и полем содержания, подписями продолжений, неразбираемой подписью, кодовыми абзацами, списками через `numPr` и через отступ, разрывами страниц, строкой библиографии без маркера, плюс полный круг `example.md` → DOCX → Markdown, каждая с переключением `keep_toc_pages` и `emit_page_breaks`) дают побайтово идентичный Markdown против дорефакторного обходчика.
Попутно убраны три мёртвых фрагмента: повторный вызов чистой функции при `parsed is None and kind`, no-op `if kind and parsed.kind != kind: pass` (из-за которого весь поиск `caption_kind_from_style` оказывался мёртвым, вместе с импортом), и `try/except` вокруг `re.match` по значению, которое всегда строка.
**Два следствия, требующие внимания.** `mdexport/` остался untracked, при этом уже прописан в `pyproject.toml` и импортируется обоими конвертерами — это пункт P0 в [TODO.md](TODO.md). И при разборе обнаружился реальный баг потери данных в обработчиках списков `word2md`, существовавший и до правок: он сохранён намеренно ради поведенческой нейтральности и описан в [TODO.md](TODO.md) с механизмом и порядком починки.
Спецификации PyInstaller править не пришлось — они идут по статическим импортам.
---
## Как это делалось
Работа шла шестью параллельными исполнителями, разведёнными по владению файлами, чтобы не наступать друг другу на правки. Волна 1: фиксы багов, кэш горячего пути, диаграммы, чистка репозитория. Волна 2: двойной рендер вместе с мёртвым кодом внутри пакета, поскольку они делят `renderer.py`. Отдельным потоком шли сиблинг-конвертеры — единственная область, не пересекавшаяся ни с чем.
Три требования, заданных жёстко всем исполнителям: правки, меняющие раскладку страницы, доказываются побайтовым сравнением вывода, а не чтением кода; вопросы Фазы 1a не трогаются без решения владельца; коммиты и деструктивные git-команды запрещены.
+84
View File
@@ -0,0 +1,84 @@
# 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 -o report.md # импорт Word → Markdown (word2md)
python -m md2gost report.docx --check-pages # только проверка вёрстки готового DOCX
python -m word2md report.docx -o report.md
```
Справка: `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` | Настройки → Файлы | титул, задание, шаблон |
| `--auto-title` / `--no-auto-title` | галочка «Автотитул» | автогенерация титула (по умолчанию выкл.) |
| `--student` / `--group` | Настройки → Студент… | ФИО и группа для автотитула |
| `--doc-author-from` / `--doc-author` | Настройки → Метаданные… | автор в свойствах DOCX (`os` / `student` / `custom`) |
| `--doc-title` / `--doc-subject` / `--doc-keywords` / `--doc-comments` / `--doc-category` / `--doc-last-modified-by` | Настройки → Метаданные… | остальные поля File → сведения |
| `--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` | Настройки → Диаграммы | локальный рендер UML / запасной Kroki |
| `--diagram-fallback` | Диаграммы | `remote` / `local` / `off` (отключает только kroki.io) |
| `--diagram-format` | Диаграммы | `png` / `svg` |
| `--diagram-scale` | Диаграммы | качество PNG PlantUML / локального Mermaid (по умолчанию 2) |
| `--page-start N` | Основные → Смещение страниц | начальный номер PAGE в теле; пусто = сквозной |
| (нет CLI) | Диаграммы → «Скачать Graphviz» | portable Graphviz для DFD (`vendor` или `%LOCALAPPDATA%\md2gost\graphviz`) |
| `--install-chromium` | Диаграммы → «Скачать headless Chromium» | Chromium для Mermaid в `%LOCALAPPDATA%\md2gost` |
| `--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, выходной путь
- **Настройки → Студент…** — ФИО и группа (при первом запуске спрашивают сами)
- **Настройки → Метаданные…** — свойства DOCX (автор, название, тема, теги, примечание)
- **Настройки → Диаграммы** — PlantUML, Graphviz (DFD), Mermaid (Chromium), Kroki, формат, кэш includes
- **Шаблоны UML** — редактор `md2gost.schemes.json`
- **Справка → Инструкция / Документация / Схемы / Промпт для ИИ**
(Документация — встроенный просмотр `docs/*.md`, вшито в exe)
## Автотитульник
Кратко: **по умолчанию выкл.** Вкл. — `--auto-title` или галочка «Автотитул». Рядом с `.md` — `info_conv.yaml`, в MD — опциональный блок `title` (`number` / `year`), ФИО и группа — **Настройки → Студент…** или `--student` / `--group`. Явный `--title` отключает генератор.
Подробно (примеры, таблица полей, приоритеты, FAQ): **[title.md](title.md)**.
## Свойства файла Word
По умолчанию в DOCX пишутся **автор = имя пользователя Windows** и примечание «Создано при помощи md2gost (ТЗ МИРЭА)». Даты создания/изменения ставятся на момент сборки, revision = 1 (чтобы не утекали 2013 год и revision из шаблона).
Задать свои поля: **Настройки → Метаданные…** (сохраняется в `md2gost.user.json`) или флаги `--doc-author-from` / `--doc-author` / `--doc-title` / `--doc-subject` / `--doc-keywords` / `--doc-comments` / `--doc-category` / `--doc-last-modified-by`. Источник автора: `os` (по умолчанию), `student` (ФИО из профиля), `custom` (строка). Пустое `--doc-comments` / пустое примечание в GUI — не писать штамп md2gost.
## Другие форматы
```bash
python -m md2fodt report.md -o report.fodt # LibreOffice
python -m word2md report.docx -o report.md # Word → Markdown (диалект md2gost)
# PDF через LaTeX — см. md2latex/
```
Импорт DOCX подробнее: [word2md.md](word2md.md). В GUI — кнопка «Импорт DOCX→MD» или drop `.docx`.
+205
View File
@@ -0,0 +1,205 @@
# Ручная чистка git
Команды для удаления мусора из репозитория. Всё под PowerShell — помни, что `&&` там не работает как разделитель, используй `;` или отдельные вызовы.
Файл описывает то, что осталось сделать руками. То, что уже сделано агентами и лежит в staging, перечислено в конце для сверки.
---
## 0. Подготовка
Чтобы git печатал кириллические имена читаемо, а не как `\320\242...`:
```powershell
git config core.quotepath false
```
Снимок текущего состояния — на случай, если захочется откатиться:
```powershell
git status --short > ..\git-status-before.txt
git stash list
```
Ничего из перечисленного ниже не переписывает историю и не трогает remote, так что откат всегда возможен через `git restore --staged <path>`.
---
## 1. `Other_Code/` — 59 файлов, 1158 КБ
Что это: схемы OOXML (`*.xsd`), скрипты pack/unpack/validate, `document.py`, `utilities.py`, `docx-js.md`, `SKILL.md`. Справочный материал.
Почему можно убирать: ни одного импорта из `md2gost`, `word2md`, `md2latex`, `md2fodt`, `tests/`, ни упоминания в `.spec`, `.bat`, `.github/workflows/`, `pyproject.toml`. Единственные ссылки — раздел 12 в `PROJECT.md` и внутренние перекрёстные ссылки самого каталога.
Проверь сам, прежде чем удалять:
```powershell
git grep -n "Other_Code" -- ':!Other_Code' ':!PROJECT.md'
git grep -nE "^\s*(import|from)\s+(scripts|validation)\b" -- 'md2gost' 'word2md' 'md2latex' 'md2fodt' 'tests'
```
Ожидаемый результат обеих команд — пусто.
### Вариант А: снять с отслеживания, файлы оставить на диске
Подходит, если хочешь держать схемы под рукой локально.
```powershell
git rm -r --cached Other_Code
Add-Content -Path .gitignore -Value "`n# Справочные схемы OOXML (не часть продукта)`n/Other_Code/"
```
### Вариант Б: удалить полностью
```powershell
git rm -r Other_Code
```
### Вариант В: вынести в отдельную ветку и потом удалить
Если материал ценен, но не должен висеть в основной ветке:
```powershell
git subtree split --prefix=Other_Code -b reference/ooxml-schemas
git rm -r Other_Code
```
Ветка `reference/ooxml-schemas` останется локально с полной историей каталога.
После любого варианта подчисти упоминание в `PROJECT.md`, раздел 12.
---
## 2. Личные документы в корне
```
Практическая_работа_6.docx 42 КБ
ТарасовИА_КР_v3.docx 2326 КБ
```
Та же категория, что и `md2gost.user.json`: персональные данные, причём ФИО прямо в имени файла. Ни одной ссылки из кода, тестов или сборочных скриптов.
Снять с отслеживания, файлы оставить на диске:
```powershell
git rm --cached "Практическая_работа_6.docx"
git rm --cached "ТарасовИА_КР_v3.docx"
```
Если кириллица в аргументах ломается, обойди её через pathspec по расширению и отбери нужное вручную:
```powershell
git ls-files "*.docx"
```
Правило в `.gitignore`, чтобы не вернулись:
```powershell
Add-Content -Path .gitignore -Value "`n# Личные документы (ФИО в имени файла)`n/*.docx`n!md2gost/Template.docx`n!md2gost/TitleTemplate.docx"
```
**Осторожно с этим правилом.** `md2gost/Template.docx` и `md2gost/TitleTemplate.docx` — живые файлы, они force-include в колесо через `pyproject.toml` и попадают в exe через `.spec`. Правило выше их исключает, но проверь результат:
```powershell
git check-ignore -v md2gost/Template.docx md2gost/TitleTemplate.docx
```
Обе строки должны вернуть пусто (то есть файлы НЕ игнорируются). Если вернули правило — убери `/*.docx` и перечисли два личных файла поимённо.
---
## 3. Проверка результата
```powershell
git status --short
git ls-files | Measure-Object | Select-Object -ExpandProperty Count
```
Размер отслеживаемого дерева до и после:
```powershell
(git ls-files | ForEach-Object { (Get-Item -LiteralPath $_).Length } | Measure-Object -Sum).Sum/1MB
```
Убедись, что ничего живого не выпало:
```powershell
.venv\Scripts\python.exe -c "import md2gost, word2md, md2latex, md2fodt; print('ok')"
$env:PYTHONIOENCODING="utf-8"; .venv\Scripts\python.exe -m md2gost --help
.venv\Scripts\python.exe -m pytest -q
```
Переменная `PYTHONIOENCODING` нужна из-за cp1251 в консоли — без неё `--help` падает на символе `→`, хотя код в порядке.
---
## 4. Про историю: снятие с отслеживания её не чистит
Это важно понимать. `git rm --cached` убирает файл из индекса и будущих коммитов, но все прошлые коммиты остаются нетронутыми. То есть:
- `md2gost.user.json` с твоими ФИО и группой **по-прежнему лежит в истории**
- `ТарасовИА_КР_v3.docx` тоже, все 2.3 МБ, в каждом коммите, где он менялся
- удалённые прототипы `mm2.py`/`mm3.py` и `Other_Code/` продолжают занимать место в `.git`
Если репозиторий приватный и таким останется — этого достаточно, дальше можно не читать.
Если планируешь публиковать, историю надо переписывать. Инструмент — `git filter-repo` (BFG устарел):
```powershell
uvx git-filter-repo --path md2gost.user.json --invert-paths
uvx git-filter-repo --path "ТарасовИА_КР_v3.docx" --invert-paths
uvx git-filter-repo --path Other_Code --invert-paths
```
Перед этим обязательно:
1. Сделай полную копию каталога `.git`, а лучше всего репозитория — операция необратима.
2. Учти, что все хеши коммитов сменятся. Если репозиторий уже где-то запушен, всем придётся клонировать заново.
3. Прогони `git reflog expire --expire=now --all; git gc --prune=now --aggressive`, иначе объекты останутся в паке.
Я специально не делал это автоматически: перезапись истории — необратимая операция, её стоит запускать осознанно и на копии.
---
## Приложение: что уже сделано агентами
Всё перечисленное лежит в staging, **не закоммичено**. Просмотри `git diff --cached` перед коммитом.
Удалено с диска (мёртвые прототипы, 6777 строк):
```
mm2.py mm3.py mm4.py mm5.py main12.py main.py
asd.py parser.py font.py _audit_tmp.py
poetry.lock requirements.txt
```
Снято с отслеживания, файлы остались на диске:
```
ReadMe.docx md2gost/README.docx
tmp_listing_word.md/.docx tmp_readme_word.docx
tmp_readme_word2.docx README.md1
examples/example_out.docx examples/toc_manual.docx
examples/toc_native.docx tests/_idef0_preview*.png/.svg
latex/mirea/Settings/FiraCode-Regular.otf
ad/ (4 файла) md2gost.user.json
```
Перенесено с сохранением истории:
```
_method_extract.txt -> docs/reference/method-mirea-2022.txt
tests/extended_mardown/ -> tests/extended_markdown/
```
Добавлено: `md2gost.user.json.example` с пустыми полями.
### Побочный эффект правил игнора, о котором стоит знать
Новые правила `/tmp_*.png` и `/tmp_*.puml` захватили рабочие черновики `tmp_org_full.*`, `tmp_org_uml.*`, `tmp_arch.*` — они пропали из `git status`, хотя на диске целы. Если какой-то из них нужен под версионным контролем, добавь исключение:
```powershell
Add-Content -Path .gitignore -Value "!/tmp_arch.puml"
```
Каталог `ai/` не тронут и остаётся untracked.
+159
View File
@@ -0,0 +1,159 @@
# Синтаксис 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`, `idef0`, `dfd`/`data-flow-diagram` (Graphviz вшитый/PATH или Kroki), или id схемы (`c4`, `usecase`, …).
- `+listing` — ещё и Листинг с исходником
- `+landscape` — альбомная страница под широкий рисунок/таблицу
Подробности: [schemes.md](schemes.md).
## Формула
```markdown
%eq1
$$
E = mc^2
$$
Зависимость (@Формула:eq1) используется далее.
```
Нумеруются **только** формулы, на которые есть `@Формула:…`.
## Ссылки на объекты
`@Рисунок:id`, `@Таблица:id`, `@Листинг:id`, `@Формула:id` — id совпадает с меткой после `%` или в title картинки.
## Источники
В тексте: `[1]`, `[2, 3]` (для ВКР — `[1.5]`). В DOCX номера становятся ссылками на пункты списка источников.
В списке:
```markdown
[1]: Иванов И. И. Название. — М.: Наука, 2024. — 120 с.
```
Во **ВВЕДЕНИИ** и **ЗАКЛЮЧЕНИИ** ссылок `[n]` быть не должно.
## Приложения
```markdown
# *ПРИЛОЖЕНИЯ
## Приложение А Листинг модуля
…
## Приложение Б Графический материал
```
Буквы: А, Б, В, Г, Д, Е, Ж, И, К… **Нельзя:** Ё, З, Й, О, Ч, Ь, Ы, Ъ.
## Тире и разрыв страницы
- Тире в предложениях: «—» (по умолчанию сохраняется). Замена на «-»: `--emdash-to-hyphen`.
- Строка `---` / `***` / `___` по умолчанию **игнорируется**. Разрыв страницы: `--hr-pagebreak` или галочка в GUI.
## Автотитульник (метаданные в MD)
В начале файла можно указать блок с оградой `title` (номер работы, год, оверрайды полей). Рядом с файлом — `info_conv.yaml` с институтом, кафедрой, дисциплиной и преподавателем. ФИО и группа студента задаются в GUI / CLI, не в yaml. Автогенерация титула **по умолчанию выкл.** — включите `--auto-title` или галочку «Автотитул».
Подробно: [title.md](title.md).
## Запрещено в отчёте
- «рис.», «табл.»
- графа «№ п/п»
- сноски `[^1]`
- формулы обычным текстом вместо `$$…$$`
- нумерация спецразделов (`# ВВЕДЕНИЕ` вместо `# *ВВЕДЕНИЕ`)
+34
View File
@@ -0,0 +1,34 @@
# Промпты для ИИ
Промпты лежат в [`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`) |
| [`emulate-student.md`](../prompts/emulate-student.md) | После любого generate-*: текст про предмет, не отчёт модели о сдаче ТЗ |
## Как пользоваться
1. Откройте промпт (файл или GUI). При необходимости добавьте `emulate-student` после базового generate-*.
2. В GUI включите нужные схемы (C4, usecase, …) — в конец промпта добавятся макросы и `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, actor/usecase и т.д.) — в том числе пользовательских из «Шаблоны UML».
## Подробнее о синтаксисе
- [markdown.md](markdown.md)
- [schemes.md](schemes.md)
- [prompts/README.md](../prompts/README.md)
+64
View File
@@ -0,0 +1,64 @@
# Быстрый старт
## Установка
Нужен Python 3.10+.
```bash
uv sync # окружение по uv.lock (канонический lock-файл)
# или
pip install -e .
```
Для продолжения таблиц/листингов через 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. При необходимости: Настройки → Файлы (титул, задание), **Настройки → Студент…** (ФИО и группа для автотитула), **Настройки → Метаданные…** (свойства DOCX), Настройки → Диаграммы, меню «Шаблоны UML».
4. Нажмите «Конвертировать».
Справка в меню: **Инструкция**, **Схемы**, **Промпт для ИИ**.
Автотитул (`info_conv.yaml` + блок `title`): [title.md](title.md).
## Типы документов (кратко)
| `--type` | Назначение |
|----------|------------|
| `practice` / `coursework` | Практика / курсовая по ТЗ МИРЭА |
| `vkr` | ВКР (раздельный список, графическое приложение) |
| `PIS_custom` | Итоговый отчёт по практическим работам |
| `APID_coursework` | Курсовая «Архитектура приложений и данных» |
Подробнее: [types.md](types.md).
## Следующие шаги
- Написать отчёт в диалекте md2gost: [markdown.md](markdown.md)
- Автотитульник: [title.md](title.md)
- Вставить диаграммы: [schemes.md](schemes.md)
- Сгенерировать `.md` через ИИ: [prompts.md](prompts.md)
+988
View File
@@ -0,0 +1,988 @@
УДК 378
ББК 65
К 43
Кириллина Ю.В. Требования к оформлению отчетов по практикам, курсовых работ и
ВКР бакалавров и магистров [Электронный ресурс]: Методические указания / Кириллина
Ю.В., Лагунова А.Д., Бергер Е.Г. — М.: МИРЭА – Российский технологический
университет, 2022. — 1 электрон. опт. диск (CD-ROM)
Методические указания знакомят студентов с требованиями по оформлению учебных
работ, таких как курсовые работы, отчеты по практикам, выпускная квалификационная
работа. Использование методических указаний позволит студентам грамотно оформлять
учебные работы, которые они выполняют согласно учебному плану в процессе обучения.
Данные методические указания основаны на ГОСТ 7.32 – 2017 Система стандартов по
информации, б иблиотечному и издательскому делу. Отчет о научно -исследовательской
работе. Структура и правила и оформления.
Предназначено для студентов по направлени ям подготовки 09.03.03 Прикладная
информатика, 09.0 4.03 Прикладная информатика , а также может быть использовано
студентами других направлений подготовки для получения навыков грамотного оформления
учебных работ.
Методические указания издаются в авторской редакции.
Авторский коллектив: Кириллина Юлия Владимировна, Лагунова Анна Дмитриевна, Бергер
Екатерина Геннадьевна
Рецензенты:
Одинцова Мария Александровна , к.т.н., доцент, доцент кафедры «Предметно -
ориентированные информационные системы» Института кибербезопасности и
цифровых технологий РТУ МИРЭА
Системные требования:
Наличие операционной системы Windows, поддерживаемой производителем.
Наличие свободного места в оперативной памяти не менее 128 Мб.
Наличие свободного места в памяти постоянного хранения (на жестком диске) не менее 30 Мб.
Наличие интерфейса ввода информации.
Дополнительные программные средства: программа для чтения pdf-файлов (Adobe Reader).
Подписано к использованию по решению Редакционно-издательского совета
МИРЭА — Российский технологический университет.
Обьем: 5.39 мб
Тираж: 10
© Кириллина Ю.В., Лагунова А.Д.,
Бергер Е.Г., 2022
© МИРЭА – Российский
технологический университет, 2022
3
СОДЕРЖАНИЕ
ПРЕДИСЛОВИЕ ...................................................................................................... 4
1. СТРУКТУРА УЧЕБНОЙ РАБОТЫ .................................................................... 5
2. ОФОРМЛЕНИЕ ТЕКСТА ДОКУМЕНТА ........................................................ 12
3. ОФОРМЛЕНИЕ ИЛЛЮСТРАЦИЙ И ТАБЛИЦ В УЧЕБНОЙ РАБОТЕ ....... 17
4. ОФОРМЛЕНИЕ ТЕКСТА (ЛИСТИНГА) ПРОГРАММЫ В УЧЕБНОЙ
РАБОТЕ .................................................................................................................. 27
5. ОФОРМЛЕНИЕ СПИСКА ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ В
УЧЕБНОЙ РАБОТЕ ............................................................................................... 30
6. ОФОРМЛЕНИЕ ПРИЛОЖЕНИЙ В УЧЕБНОЙ РАБОТЕ .............................. 34
СПИСОК ИСТОЧНИКОВ И ЛИТЕРАТУРЫ ...................................................... 37
ПРИЛОЖЕНИЕ А .................................................................................................. 38
ПРИЛОЖЕНИЕ Б................................................................................................... 39
ПРИЛОЖЕНИЕ В .................................................................................................. 41
ПРИЛОЖЕНИЕ Г................................................................................................... 42
ПРИЛОЖЕНИЕ Д .................................................................................................. 44
ПРИЛОЖЕНИЕ Е .................................................................................................. 45
4
ПРЕДИСЛОВИЕ
В процессе обучения студентам направлений подготовки 09.03.03, 09.04.03
Прикладная информатика необходимо писать и оформлять курсовые работы, от-
четы по различным видам и типам практик, а также выпускную квалификацион-
ную работу (далее по тексту — учебные работы). Данные методические указания
предназначены для студентов вышеуказанных направлений подготовки.
Целью данных методических указаний является поддержка грамотного
оформления текстовых документов студентами.
Задачами методических указаний являются:
• ознакомление студентов с общей структурой учебных работ, таких как
курсовая работа, отчет по практике, выпускная квалификационная ра-
бота (ВКР);
• ознакомление студентов с правилами оформления учебных работ.
Основой для создания методических указаний является ГОСТ 7.32 — 2017
Система стандартов по информации, библиотечному и издательскому делу.
Отчет о научно-исследовательской работе. Структура и правила и оформления.
Методические указания составлены в соответствии с:
• СМКО МИРЭА 7.5.1/03.П.67- 18 Положение о выпускной квалифика-
ционной работе студентов, обучающихся по образовательным про-
граммам подготовки бакалавров;
• СМКО МИРЭА 7.5.1/03.П.68- 18 Положение о выпускной квалифика-
ционной работе студентов, обучающихся по образовательным про-
граммам подготовки магистров;
• СМКО МИРЭА 7.5.1/04.И.05- 18 Инструкция по организации и прове-
дению курсового проектирования.
5
1. СТРУКТУРА УЧЕБНОЙ РАБОТЫ
Учебная работа студента должна иметь определенную структуру.
Структура конкретной учебной работы регламентируется методическими
указаниями по написанию курсовой работы, методическими указаниями по
написанию отчета по практике, методическими указаниями по написанию ВКР,
которые разрабатываются преподавателями выпускающей кафедры и издаются
редакционно-издательским отделом университета. В се изданные методически е
указания выпускающей кафедры студент может найти на сайте университет а в
разделе Студентам, далее Библиотека.
В тоже время общая структура учебной работы включает в себя:
• ый лист;
• ие;
• ие;
• ие;
• ием;
• ие;
• иков;
• ия.
Титульный лист учебной работы имеет утвержденную форму , выдается
преподавателем, который руководит курсовой работой/практикой/ВКР.
На титульном листе в обязательном порядке проставляется подпись
руководителя учебной работы и студента. В случае, если в кач естве учебной
работы выступает курсовая работа, то на не м необходима подпись второго
преподавателя, участвующего в процедуре защиты курсовой работы. В слу чае,
если в качестве учебной работы выступает ВКР, то на титульном листе
необходима подпись заведующего выпускающей кафедрой. Примеры титульных
листов различных учебных работ приведены в Приложении А .
му и выдается
преподавателем, который руководит курсовой работой/практикой/ВКР.
аботы) — т, в
яется абота). ие подписывается
аботы. совых
абот, а т а к ж е о т ч е т о в п о п р а к т и к е ие обязательно ждается
ой. абот
дополнительно согласуется с директор ом института. Факт принятия
студентом задания к работе подтверждается подписью студента на задании.
6
Кроме подписей в задании должны быть указаны дата утверждения задания
заведующим кафедрой, дата выдачи задания, дата принятия задания к
исполнению. Примеры заданий различных учебных работ приведены в
Приложении Б.
афической
частей в учебной работе устанавливаются методическими указаниями по
написанию курсовой работы, методическими указаниями по написанию отчета
по практике, методическими указаниями по написанию ВКР .
авливается методическими
указаниями по написанию курсовой работы, методическими указаниями по
написанию отчета по практике, методическими указаниями по написанию ВКР ,
а также положениями, приведенными в предисловии данного пособия.
Текст основной части учебной работы еобходимости а азделы,
азделы, пункты . кты имеют заголовки, четко и
ктов.
а листе, который расположен ого листа и
ия, азделов,
иц. иц
а быть
ая.
ия заголовков:
• аздела — я;
• аздела — я;
• ктов — ей.
та,
ые с абзацевого отступа.
аков
ия пишется заголовок.
а б ы т ь в
ﮦ ﮦ аздела,
ых точками.
Параметры оформления заголовков в учебной работе представлены в табл.
2.1. Важно обратить внимание на то, что интервалы между заголовками
выставляются в единицах измерения — мм, а не пт.
На рис. 2.1 представлен пример внешнего вида заголовков выпускной
квалификационной работы и курсовой работы с учетом установленных
параметров страниц для учебной работы.
7
Таблица 2.1. Параметры оформления заголовков в учебной работе
Параметр Заголовок
первого урﮦовнﮦя
Заголовок
второго уровня
Заголовок
третьего уровня
ифт
Шрﮦифт Times New Roman
Нﮦачерﮦтанﮦие полужирﮦнﮦый
Подчерﮦкиванﮦие нﮦет
Видоизменﮦенﮦие все прﮦописнﮦые нﮦет нﮦет
Рﮦазмерﮦ 18 пт 16 пт 14 пт
Цвет текста Авто (черﮦнﮦый)
Абзац
Вырﮦавнﮦиванﮦие По левому крﮦаю
Перﮦвая стрﮦока нﮦет
Отступ слева 1,25 см
Отступ спрﮦава 0 мм
Инﮦтерﮦвал перﮦед 0 мм 15 мм 15 мм
Инﮦтерﮦвал после 10 мм 10 мм 10 мм
Междустрﮦочнﮦый
инﮦтерﮦвал
Полуторﮦнﮦый
Положенﮦие нﮦа
ице
Не отрывать от
следующего
С новой страницы
Не отрывать от
следующего
Запрет висящих строк
Нﮦе отрывать от
следующего
Запрет висящих строк
Рисунок 2.1. Пример внешнего вида заголовков
8
азделы «СОДЕРЖАНИЕ», «ВВЕДЕНИЕ», «ЗАКЛЮЧЕНИЕ», «СПИСОК
ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ », «П РИЛОЖЕНИЕ»/«ПРИЛОЖЕНИЯ»
уются.
Разделы «СОДЕРЖАНИЕ» и «СПИСОК ИСПОЛЬЗОВАННЫХ
ИСТОЧНИКОВ» выравниваются по центру страницы.
ие
азделяют точкой.
е Word используется
вкладка «Стили» (рис. 2.2). ﮦ ов
о
ить азу
ия.
Рисунок 2.2. Использование вкладки «Стили» в текстовом редакторе Word
ить
а «Стили» ять
овые стили (рис. 2.3).
ые соответствующими стилями, позволяют
ие.
ктов должны
ии, и
соответственно наоборот. Для обеспечения этого содержание учебной работы
должно формироваться с помощью встроенной функции Оглавление.
ИЕ»,
ием по
у. Важно обратить внимание на значения интервала перед самим
заголовком и после него для того, чтобы обеспечить нужные интервалы между
заголовков и последующими пунктами самого содержания учебной работы.
ю
«Ссылки» → а ю и с
ителя строки «. . . . . . . .» ( рис. 2.4).
9
ить и
овать.
Рисунок 2.3. Редактирование заголовков в текстовом редакторе Word с
использование диалогового окна «Стили»
Рисунок 2.4. Формирование содержания через Автособираемое оглавление
иц,
ом
овить поле».
10
При создании содержания слово ОГЛАВЛЕНИЕ заменяется в ручном
режиме на СОДЕРЖАНИЕ с выставлением необходимых параметров,
характерных для заголовков первого уровня.
ой
текст» без отступа первой строки аю.
я,
ии
ыми буквами (рис. 2.5), аналогично тому, как они
оформлены в учебной работе за исключением того, что полужирное начертание
не применяется.
Рисунок 2.5. Пример оформления содержания в ВКР
11
ии текст попадает иц
кте отсутствует отточие ицы, то часть
очку.
12
2. ОФОРМЛЕНИЕ ТЕКСТА ДОКУМЕНТА
ты учебной работы ой
е листа.
В случае представления учебной работы в бумажном виде она а быть
мата А4.
Учебная работа ых
ﮦ илами.
Учебная работа проверяется руководителем на соблюдение правил
оформления, в случае несоответствия работы она не допускается до защиты и
выставления оценки, и направляется на доработку.
Текст документа должен быть кратким, четким и не допускать различных
толкований. В докуме нтах должны применяться научно-технические те рмины,
обозначения и определен ия общеп ринятые в научно-технической лите ратуре
Стиль изложения текста в учебной работе строго нау чный, подстиль —
собственно-научный. е допускается:
• ицизмы,
ализмы;
• ия;
• усской
тами и
ая
овка;
• и
ﮦ .
Текст учебной работы печатается, соблюдая следующие размеры полей:
левое — 30 мм, правое — 10 мм, верхнее и нижнее — 20 мм. Абзацный отступ
должен быть одинаковым по всему тексту и равен 1,25 см [1].
а листах в рамках еще
незаконченного раздела (см. Приложение В).
иц.
Не допускается размещать заголовки в кон це страницы, а основной текст
переносить на следующую страницу.
Текст ифтом
ого цвета , размер шрифта — 14 пт, тип шрифта для основного текста
учебной работы — Times NewтRoman.
ой текст» (табл. 2.1).
13
ифт»,
а знаков абзаца и других скрытых знаков
форматирования ца абзаца и
аки» (ая» →
ифт» → ¶).
Таблица 2.1. Параметры оформления основного текста учебной работы
Параметр Значение
ифт
Шрﮦифт Times New Roman
Нﮦачерﮦтанﮦие Обычнﮦый
Подчерﮦкиванﮦие нﮦет
Видоизменﮦенﮦие нﮦет
Рﮦазмерﮦ 14 пт
Цвет текста Авто (черный)
Абзац
Выравнивание По ширине
Первая строка 1,25 см.
Отступ слева нет
Отступ справа нﮦет
Интервал перﮦед нﮦет
Интервал после нет
Междустрочный интервал Полуторный
Положение на странице Запрет висящих строк
Пробел ставится после любого знака пунктуации, но не перед ним. При этом
не ставятся п робелы после отк рывающих скобок и кавычек, так же как и пе ред
закрывающими скобками и кавычками.
Кавычки в русском тексте (включая список использова нных источ ников)
имеют форму «…», а в английском — "…".
ая.
ицы учебной работы оставляемыми по
ифт — ﮦ12 пт).
ﮦ ицы
е ставится:
• титульный лист;
• ие;
• ие.
е
аки, такие как
«+», «–», «=», «>», «<» и так далее. В тексте их следует писать словами: «плюс»,
ьше».
аки:
14
1. ак дефиса «-» обелами и используется:
• жащих дефисы (во-первых,
физико-математический, кто-либо);
• валов (100-150 Мб).
2. уса «–» ия.
3. е «—обелами и используется в соответствии с
ктуации (Классификаторﮦ —это
систематизированный …).
абские
талов, полугодий,
ами.
ами, ы
иями (… в начале XXI века).
ых
ий (… во 2 разделе, … характерно для середины 1980 годов).
ﮦ ой и той же
его числа
(…составляет 200, 250, 300 руб.)ица
ﮦ е (… длиной 20-25 см).
ﮦ иц
ﮦ ами, а числа без
ﮦ ицы до девяти
— словами (провести испытан ия семи т руб, каждая дли ной 5 м) . ые
обей с ипользованием
запятой при написании (20,5 см).
ый и ый спис ки уются стилем
ой текст».
Таблица 2.2. Параметры оформления списков
Параметр Значение
Марﮦкирﮦованﮦнﮦый список –, ―, ●, ■, ○
Нﮦумерﮦованﮦнﮦый список 1.
а)
Положенﮦие марﮦкерﮦа (нﮦомерﮦа) Отступ 1,25 см
Положенﮦие текста Табуляция после 2,25 см
Отступ 2,25 см
ﮦ а во всем
ькой
15
чивается — кт в
чивается точкой).
ﮦ ой буквы и
чиваться точкой (рис. 2.1-2.3).
Рисунок 2.1. Пример 1 оформления списка в учебной работе
Рисунок 2.2. Пример 2 оформления списка в учебной работе
16
Рисунок 2.3. Пример 3 оформления списка в учебной работе
В случае необходимости применения для нумерованного списка строчных
букв русского алфавита, список начинают с буквы «а» (за исключением букв ё,
з, й, о, ч, ъ, ы, ь) [1].
Внутри списка не могут располагаться иллюстрации и таблицы.
При выполнении учебной работы необходимо соблюдать равномерную
плотность и четкость изображения по все й работе. Все линии, буквы, цифры и
знаки должны иметь одинаковую контрастность по всему тексту.
17
3. ОФОРМЛЕНИЕ ИЛЛЮСТРАЦИЙ И ТАБЛИЦ В
УЧЕБНОЙ РАБОТЕ
Количество иллюстраций в учебной работе должно быть достаточ ным для
пояснения излагаемого текста, в некоторых случаях обязательные иллюстрации
регламентируются методическими указаниями по написанию курсовой работы,
методическими указаниями по написанию отчета по практике, методическими
указаниями по написанию ВКР.
о после
ице (по
возможности ближе к соответствующим частям текста учебной работы) с
у без абзацного отступа и первой строки.
ами.
аздела —
ﮦ ﮦ азделяются точкой.
ации
ации (табл. 3.1).
Таблица 3.1. Параметры оформления подписи иллюстраций
Параметр Значение
ифт
Шрифт Times New Roman
Начертание Полужирﮦнﮦый
Подчеркивание нет
Видоизменﮦение нет
Размер 12 пт
Цвет текста Авто (черﮦнﮦый)
Абзац
Выравнивание По центру
Первая строка 0 см
Отступ слева 0 см
Отступ спрﮦава нет
Интервал перﮦед 0 мм
Интервал после 6 мм
Междустрочный интервал Одинарный
Положение на странице Запрет висящих строк
ия
е ставится . Предложение, в котором автор ссылается на
рисунок, должно заканчиваться точкой, а не двоеточие м, т а к к а к э т о является
нарушением правил пунктуации. (рис. 3.1).
ием
а.
18
Рисунок 3.1. Пример оформления рисунка с учетом установленных параметров
страниц учебной работы
углых скобках или
ﮦ :
• план проекта иллюстрирует диаграмма Ганта (Рисунок 1.1);
• дерево функций представлено на Рисунке 1.1.
о
ия по часовой
а 90 яется с
ую с помощью команды главного меню «Макет» . При
размещении рисунка на отдельной странице с альбомной ориентацией
необходимо установить в Параметрах страницы вертикальное выравнивание
по центру.
Страницы с альбомной ориентацией предназначены только для
рисунков и таблиц, нельзя размещать на них основной текст (Приложение Г,
рис. Г.1-Г.2).
19
Сначала устанавливаются разрывы страницы, начиная со следующей
страницы, а потом меняется ориентация с книжной на альбомную (рис. 3.2-3.3).
Рисунок 3.2. Переход от книжной ориентации к альбомной
Рисунок 3.3. Пример оформления рисунка на странице с альбомной ориентацией
20
В случае, если рисунок завершает раздел, подраздел или пункт
рекомендуется после него сформулиро вать заключительное предложение. Для
раздела это будет заключительное предложение, подводящее итоги по
выполненной работе в соответствующем разделе. Для подраздела или пункта это
будет завершающее предложение, обеспечивающее переход к следующему
подразделу или пункту (рис. 3.4-3.5).
Рисунок 3.4. Пример завершения раздела и переход к следующему
Все иллюстрации должны иметь границы четко по представляемому
рисунку, схеме и т.д., и не допускается пустая, неинформативная часть в
иллюстрации, занимающая место, предназначеннное для основного текста.
На рис. 3.6 представлена правильная и неправильная обрезка границ
иллюстрации.
Вне зависимости от того, каков формат представления учебной работы:
распечатанная бумажная версия или электронный файл, все иллюстрации
должны удовлетворять требованию их четкого воспроизведения, то есть должна
быть обеспечена читабельность иллюстраций без необходимости прибегать к
увеличению масштаба представленного материала.
21
Рисунок 3.5. Пример завершения подраздела и переход к следующему подразделу в
курсовой работе
Рисунок 3.6. Пример правильной обрезки границ иллюстрации (левая часть) и
неправильной обрезки границ илллюстрации (правая часть)
22
Кроме того, рисунки, представляющие собой чертежи, графики, диаграммы,
схемы, помещаемые в учебной работе , должны соответствовать требованиям
стандартов Единой системы конструкторской документации (ЕСКД) .
ия
показателей.
ие таблицы атким.
ад таблицей.
ие таблицы пишется без абзацного отступа и первой строки у
це
е ставится.
ами аздела —
ﮦ ﮦ азделяются точкой.
ом
еобходимости,
ту.
ﮦ ие ячеек
е допускается.
ед
е Word).
Параметры оформления таблицы в учебной работе представлены в табл. 3.2.
Таблица 3.2. Параметры оформления таблицы
Параметр Надпись таблицы Текст таблицы
ифт
Шрифт Times New Roman
ие Курсив Обычный, полужирный,
курсив
Подчеркиванﮦие нет
Видоизменение нет
Размер 12 пт
Цвет текста Авто (черный)
Абзац
Выравнивание По левому краю По центру, по левому
краю
Первая стрﮦока 0 см
Отступ слева 0 см
Отступ справа нет
Интервал перед 6 мм 0 мм
Интервал после 0 мм
Междустрочный интервал Одинﮦарный
Положение нﮦа странице Запрет висящих строк
23
а все таблицы учебной работы ы ссылки в тексте.
углых скобках или включаются в
ие. Если ссылка на таблицу завершает предложение, то в конце
предложения ставится точка.
ием
углых скобках
ﮦ :
• далее представлен планﮦпроекта (Таблица 3.1);
• планﮦпроекта представлен в Таблице 3.1 (рис. 3.7).
Рисунок 3.7. Пример представления таблицы
ие
ую
угими частями пишется
адпись таблицы»
(рис. 3.8).
Не допускаются таблицы в одну строчку, а также не допускается оставлять
на странице только заголовок к таблице или заголовок с заголовками граф
(Приложение Г, рис. Г.3).
ации
ед их
ием.
Все таблицы в учебной работе должны быть оформлены единообразно с
точки зрения применяемого стиля для оформления заголовков и подзаголовков
граф, а также строк таблицы.
24
Рисунок 3.8. Пример оформления переноса таблицы в учебной работе
ия,
ия
е
о под
мулой.
ием
мул Microsoft Equation или MathType.
а вкладке «Вставка» →«Уравнение» (рис.
3.9).
Рисунок 3.9. Возможности контекстного меню «Уравнение»
25
Не допускается формулы оформлять как основной текст, основным
шрифтом.
мулы, ивается по
у. а
ока.
а ака
ус ( –ия (×) или
ия яется.
ию.
тов следует
ости, в какой
муле.
ают без абзацного отступа со слова «где»
его.
Значение каждого символа и числового коэффицие нта следует давать с
новой строки, располагая символы один под другим (рис. 3.10).
Рисунок 3.10. Пример оформления последовательности формул в учебной работе
26
Если формула является концом предложения, то ставится точка, если после
формулы предложение продолжается — запятая.
Формулы нумеруются последовательно в пределах раздела арабскими
цифрами в круглых скобках, выровненных по правому краю. Нумеруются только
те формулы, на которые имеются ссылки в тексте.
Ссылки в тексте на номер формулы даются в круглых скобках.
Вне зависимости от способа выполнения учебной работы качество
напечатанного текста и оформления иллюстраций, таблиц, распечаток программ
должно удовлетворять требованию их четкого воспроизведения.
27
4. ОФОРМЛЕНИЕ ТЕКСТА (ЛИСТИНГА) ПРОГРАММЫ В
УЧЕБНОЙ РАБОТЕ
В учебной работе могут содержаться отдельные фрагменты программ (ли-
стинг), разработанных студентом.
Название листинга пишется в одну строку с номером, через тире, с большой
буквы, перед листингом. Точка в конце не ставится. Пример, «Листинг 1.7 —
Процедура открытия программы».
При ссылке на отдельные строки программы они нумеруются в порядке воз-
растания с шагом 10. После ссылки на листинг в фигурных скобках привод ятся
номера строк. Пример «Листинг 3.2 {30, 50, 140}», «Листинг 3.2 {20- 80}».
Листинг оформляется в рамку (рис. 4.1-4.2).
Рисунок 4.1. Пример 1 оформления листинга
Рисунок 4.2. Пример 2 оформления листинга
28
Параметры оформления листинга представлены в табл. 4.1.
Таблица 4.1. Параметры оформления листинга
Листинг
Надпись листинга
Шрифт Шрифт
Шрифт Times New Roman
Начертание Курсив
Размер 12 пт
Цвет текста Авто (черный)
Подчеркивание нет
Видоизменение нет
Интервал
Масштаб 100%
Интервал Обычный
Смещение Нет
Анимация Вид Нет
Абзац Отступы и интервалы
Выравнивание По левому краю
Отступ слева 0 см
Отступ справа 0 см
Первая строка нет
Интервал перед 6 мм
Интервал после 0 мм
Междустрочный Одинарный
Положение на странице Разбивка на страницы Не отрывать от следующего
С новой страницы
Содержание листинга
Шрифт Шрифт
Шрифт Courier New
Начертание Обычный
Размер 10 пт
Цвет текста Авто (черный)
Подчеркивание Нет
Видоизменение нет
Интервал
Масштаб 100%
Интервал Обычный
Смещение Нет
Анимация Вид Нет
Абзац
Отступы и интервалы
Выравнивание По левому краю
Отступ слева 0 см
Отступ справа 0 см
Первая строка 0 см
Интервал перед 0 мм
Интервал после 0 мм
Междустрочный Одинарный
29
Основной текст (строка) после листинга печатается с интервалом перед 6
мм.
Весь текст программы целесообразно выносить в приложени е.
30
5. ОФОРМЛЕНИЕ СПИСКА ИСПОЛЬЗОВАННЫХ
ИСТОЧНИКОВ В УЧЕБНОЙ РАБОТЕ
Все заимствованные части учебной работы (автором которых не является
студент) должны быть сопровождены ссылкой на источник , тип ссылки —
затекстовая (см. ГОСТ Р 7.0.5- 2008). Список должен содержать сведения об
источниках, использованных при составлении учебной работы [1].
Список использованных источников должен включать библиографические
записи на документы, использованные при составлении учебной работы, ссылки
на которые оформляют арабскими цифрами в квадратных скобках.
Сведения об источниках следует располагать в порядке появления ссылок
на источники в тексте учебной работы и нумеровать арабскими цифрами с
точкой и печатать с абзацного отступа в 1,25 см.
При оформлении раздела СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ
на первой строке листа печатается соответствующее наименование р аздела,
отформатированное по параметрам для заголовка первого уровня.
Для учебных работ вида курсовой работы или отчета по практике список
использованных источников делить на разделы не требуется. Требованиями к
источникам в данном случае являются следующие:
• количество источников от 5 до 7;
• год издания источника — за последние 5 лет к моменту написания и
представления учебной работы (Приложение Д, рис. Д.1).
В случае выпускной квалификационной рабо ты список использованных
источников делится на разделы, в каждом и которых должно быть представлено
не менее 10 источников, годом издания не больше, чем 5 лет к моменту защиты
выпускной квалификационной работы.
Параметры оформления заголовков разделов в списке использованных
источников представлены в табл. 5.1.
Внутри раздела источники распределяются в порядке упоминания их в
выпускной квалификационной работе и нум еруются (рис. 5.1). В данном случае
номер источника будет состоять из номера разд ела, к которому он относится, и
номера по порядку.
ики следует давать в виде
арабских цифр, заключенных в квадратные скобки, указывающих порядковый
номер источника по списку, например, [1], [2, 3], [1.5], [2.18].
В случае цитирования указываются страницы книги, статьи или другого
источника, из которых взята цитата, например, [2.18, c. 21-25].
31
Таблица 5.1. Параметры оформления заголовков разделов
в списке использованных источников
Параметр Значение
Шрифт
Шрифт Times New Roman
Начертание Обычный
Подчеркивание нет
Видоизменение Все прописные
Размер 14 пт
Цвет текста Авто (черный)
Абзац
Выравнивание По центру
Первая строка нет
Отступ слева 1,25 см.
Отступ справа нет
Интервал перед 6 мм
Интервал после 6 мм
Междустрочный интервал Полуторный
Положение на странице Не отрывать от следующего
Рисунок 5.1. Пример оформления списка использованных источников с делением на
разделы
32
На рис. 5. 2 приведен пример основного текста учебной работы со ссылкой
на источник.
Рисунок 5.2. Пример оформления ссылок в тексте учебной работы
На рис. 5. 3 приведен пример основного текста учебной работы со ссылкой
на источник при цитировании. При цитировании цитируемая фраза ставится в
кавычки.
Рисунок 5.3. Пример оформления ссылок в тексте при цитировании.
33
В основном тексте ВВЕДЕНИЯ и ЗАКЛЮЧЕНИЯ ссылки на
источники не указываются.
Более подробно порядок оформления различных источников представлен в
ГОСТ 7.32 — 2017 Система стандартов по информации, библиотечному и
издательскому делу. Отчет о научно -исследовательской работе. Структура и
правила и оформления. Приложение Е на странице 26 указанного документа.
34
6. ОФОРМЛЕНИЕ ПРИЛОЖЕНИЙ В УЧЕБНОЙ РАБОТЕ
яющий текст учебной работы, допускается помещать в
ие учебной работы а
последующих ее листах.
иложения могут выноситься:
• ом, так и
ия;
• уемого объекта
ия;
• асчеты;
• сложные алгоритмы;
• аботы;
• тов;
• тов;
• экранные формы инормацинной системы;
• листинги программ;
• оизводство
ии).
мата А4. Допускается
мата A3 по ГОСТ 2.301.
В учебной работе приложение может быть одно или несколько.
В случае, если в учебной работе будет только одно приложение , т о н а
следующем листе после списка использованных источников заголовком первого
уровня оформляется заголовок ПРИЛОЖЕНИЕ. И на этом же листе размещается
необходимый дополняющий материал (рис. Д.1, Приложение Д).
В случае необходимости оформить несколько приложений в учебной работе
их а я с А , з а
ﮦ «Приложение А».
ь
ия в
ой текст» (рис.
6.1).
ием
ого стилем
я».
35
Рисунок 6.1. Пример оформления раздела ПРИЛОЖЕНИЯ
а
ﮦ «Приложение А.1», «Приложение А.2», если они связаны по
смыслу.
ый по
ой текст» (рис. 6.2).
Рисунок 6.2. Пример оформления отдельного приложения в составе раздела
ПРИЛОЖЕНИЯ
36
Таблицы и иллюст рации, соде ржащиеся в п риложении, нумеруются
аналогично таблицам и рисунк а м в о сновном тексте учебной работы.
Иллюстрации каждого пр иложения обоз начают отдель ной нумерацией
арабскими циф рами с добавлен ием пе ред циф рой обоз начения приложе ния,
например, «… представлено на Рисунке Б.1», «… отражен в таблице В.1».
В тексте докумен та на все п риложения должн ы быть да ны ссылки.
Приложения располагают в порядке ссылок на них в тексте учебной работы.
При ссылке следует писать слово «П риложение» с большой буквы с
указанием порядкового номера, например «…представлено в Приложении Б».
Перечень материала, оформляемого в виде приложения, определяется
обучающимся совместно с руководителем на основе методических
рекомендаций по курсовой работе, методических рекомендаций по подготовке
отчета по практике, методических рекомендаций по написанию ВКР.
Применительно к ВКР бакалавра или магистра обязательным приложением
к ней является приложение под названием Графический материал, который
представляет собой слайды презентации с основными результатами,
полученными в ходе написания ВКР.
37
СПИСОК ИСТОЧНИКОВ И ЛИТЕРАТУРЫ
1. ГОСТ 7.32 — 2017 Система стандартов по информации,
библиотечному и издательскому делу. Отчет о научно- исследовательской
работе. Структура и правила и оформления — URL:
https://www.rea.ru/ru/org/managements/orgnirupr/Documents/gost_7.32 -2017.pdf
(дата обращения 20.11.2022)
38
ПРИЛОЖЕНИЕ А
Рисунок А.1. Общий вид титульного листа ВКР
Рисунок А.2. Общий вид титульного листа курсовой работы
39
ПРИЛОЖЕНИЕ Б
Рисунок Б.1. Общий вид одностороннего задания на курсовую работу
Рисунок Б.2. Общий вид двустороннего задания на практику
40
Рисунок Б.3. Общий вид двустороннего задания на ВКР (лицевая сторона)
Рисунок Б.4. Общий вид задания на ВКР (оборотная сторона и окончание задания )
41
ПРИЛОЖЕНИЕ В
Рисунок В.1. Пример недопустимой ситуация в учебной работе, полупустая страница при условии,
что подраздел еще не закончился
42
ПРИЛОЖЕНИЕ Г
Рисунок Г.1. Пример 1 неправильного использования страницы с альбомной
ориентацией
Рисунок Г.1. Пример 2 неправильного использования страницы с альбомной
ориентацией
43
Рисунок Г.3. Неверное представление таблицы в учебной работе
44
ПРИЛОЖЕНИЕ Д
Рисунок Д.1. Пример оформления списка использованных источников без деления на
разделы
45
ПРИЛОЖЕНИЕ Е
Рисунок Е.1. Оформление ПРИЛОЖЕНИЯ в учебной работе
46
СВЕДЕНИЯ ОБ АВТОРАХ
Бергер Екатерина Геннадьевна, к.э.н., доцент кафедры практической и при-
кладной информатики Института информационных технологий
Кириллина Юлия Владимировна, к.э.н., доцент, доцент кафедры практиче-
ской и прикладной информатики Института информационных технологий
Лагунова Анна Дмитриевна, к.э.н., доцент кафедры практической и при-
кладной информатики Института информационных технологий
+272
View File
@@ -0,0 +1,272 @@
# Схемы и диаграммы
В отчёте блок кода с языком диаграммы превращается в **Рисунок** (PNG или SVG). PNG руками экспортировать не нужно: md2gost вызывает PlantUML (jar / Kroki), локальный Mermaid (браузер / QuickJS), локальный IDEF0 (Pillow) или DFD через [data-flow-diagram](https://github.com/pbauermeister/dfd) (вшитый / системный Graphviz или Kroki).
## Синтаксис в 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: браузер / QuickJS / Kroki |
| `` ```idef0 `` / `` ```uml-idef0 `` | IDEF0 (FIPS 183): локальный рендер, без PlantUML / Kroki |
| `` ```dfd `` / `` ```uml-dfd `` / `` ```data-flow-diagram `` | DFD ([pbauermeister/dfd](https://github.com/pbauermeister/dfd)): вшитый Graphviz / PATH / Kroki |
## Встроенные схемы
Файл шаблона: [`md2gost/diagrams/schemes.json`](../md2gost/diagrams/schemes.json). При первом запуске копируется в `md2gost.schemes.json` рядом с приложением. При каждом запуске встроенные схемы (`author: md2gost`) синхронизируются с шаблоном: новые id добавляются, устаревшие (например удалённый `bpmn`) убираются, содержимое обновляется. Схемы с пустым или другим `author` не трогаются. Если правите встроенную в GUI и сохраняете — `author` сбрасывается, чтобы миграция не затёрла правки.
| id | Название | Оградка |
|----|----------|---------|
| `c4` | C4 Container | `` ```uml-c4 `` / `` ```c4 `` |
| `c4context` | C4 Context | `` ```uml-c4context `` |
| `c4component` | C4 Component | `` ```uml-c4component `` |
| `usecase` | Прецеденты | `` ```uml-usecase `` / `` ```usecase `` |
| `archimate` | ArchiMate 3.2 | `` ```uml-archimate `` / `` ```archimate `` |
Для схем в теле пишите **только макросы** (Person, Rel, actor, Business_Actor, …) — без `@startuml` и без `!include`, если схема сама их добавляет.
### C4 (пример макросов)
`Person`, `System`, `System_Ext`, `Container`, `ContainerDb`, `Rel` / `Rel_R`…
### Use Case
`actor`, `usecase`, связи `-->`, `<<include>>`, `<<extend>>`.
### ArchiMate 3.2
`Business_Actor`, `Application_Service`, `Technology_Node`, `Rel_Serving` / `Rel_Realization` / `Rel_Assignment`, …
Подробно: [archimate.md](archimate.md).
Полные шпаргалки — поля `docs` и `ai-prompt` в JSON; в GUI: **Шаблоны UML** и кнопки схем в **Справка → Промпт для ИИ**.
## IDEF0 (FIPS 183)
Оградка `` ```idef0 `` (то же: `` ```uml-idef0 ``). Текст — не XML: шапка формы, боксы, стрелки ICOM (Input слева, Control сверху, Output справа, Mechanism снизу). Контекст A-0 — один бокс; декомпозиция — несколько боксов «лесенкой» и связи между ними. Рендер локальный (Pillow), без Java и Kroki.
````markdown
%a0 Контекстная диаграмма A-0
```idef0
title Распорядиться товаром
node A-0
number 1
author Кинзябулатов Рамиль
project Разработка универсальной модели торгового предприятия
date 08.02.2022
rev 31.03.2022
status PUBLICATION
context TOP
[A0] Распорядиться товаром
<- Спрос
^ Нормативная документация
-> Товар
v Персонал
```
````
Шапка/подвал NIST-формы (необязательные поля): `title`, `node`, `number`, `author`, `project`, `date`, `rev`, `status` (`WORKING` / `DRAFT` / `RECOMMENDED` / `PUBLICATION`), `context` (для A-0 — `TOP`), `used_at`, `reader`, `reader_date`, `notes`, `purpose`, `viewpoint`, `page`, `form` (`kit` по умолчанию, `plain` — без рамки).
Бокс: `[A0] Имя` или `box A0 Имя`. Стрелки к последнему боксу: `<-` / `in` вход, `^` / `control` управление, `->` / `out` выход, `v` / `mech` механизм. Явно: `A0 <- Спрос`.
Декомпозиция A0 (3 бокса, развилка, O→C, ICOM на рамке):
````markdown
%a0 Декомпозиция A0
```idef0
title Распорядиться товаром
node A0
number 2
author Кинзябулатов Рамиль
project Разработка универсальной модели торгового предприятия
date 08.02.2022
rev 31.03.2022
status PUBLICATION
context A-0
purpose Обеспечить наличие товара и его отпуск покупателю
viewpoint Руководитель торгового предприятия
[A1] Принять и оценить спрос
[A2] Закупить и хранить
[A3] Отгрузить товар
A1 <- Спрос [I1]
A2 <- Предложения поставщиков [I2]
A1, A2, A3 ^ Нормативная документация [C1]
A1 ^ Ассортиментная политика [C2]
A3 ^ Условия поставки [C3]
A1 -> A2 : план закупок
A1 -> A2.C : график поставок
A2 -> A3 : товар к отгрузке
A3 -> Товар [O1]
A3 -> Акт отгрузки [O2]
A1, A2, A3 v Персонал [M1]
A2 v Склад [M2]
```
````
Подписи к стрелкам — существительные рядом со стволом, не на линии; если связь не очевидна, рисуется squiggle (FIPS 183). Длинные фразы переносятся на 2–3 строки.
Короткий каркас:
```
[A1] Принять заказ
[A2] Отгрузить
A1 <- Заявка [I1]
A1 ^ Правила [C1]
A1 -> A2 : заказ
A1 -> A2, A3 : данные
A1, A2 -> A3 : сводка
A2 -> Товар [O1]
A2 v Склад [M1]
```
Туннель (скобки IDEF0): `(in Секрет` — на границе листа, `in) Локальный` — у бокса. Вызов другой модели: `call ИмяМодели`. Сторона порта: `A1.O -> A2.C : условие`.
## DFD (data-flow-diagram / SA/RT)
Оградка `` ```dfd `` (то же: `` ```uml-dfd ``, `` ```data-flow-diagram ``, `` ```yourdon ``). Синтаксис — пакет [pbauermeister/dfd](https://github.com/pbauermeister/dfd) (ставится с md2gost, Python 3.11+). Рендер: Graphviz (`dot` / `neato`) или Kroki (`graphviz`), не Pillow.
Порядок поиска Graphviz (как у `plantuml.jar`):
1. Вшитый `md2gost/vendor/graphviz` (exe-сборка / `python scripts/fetch_graphviz.py`)
2. Кэш `%LOCALAPPDATA%\md2gost\graphviz` (GUI: **Настройки → Диаграммы → «Скачать Graphviz»**)
3. `dot` / `neato` в `PATH`
4. Локальный Kroki → `https://kroki.io` при `--diagram-fallback remote`
````markdown
%dfd0 Контекстная DFD
```dfd
style context
entity Client Клиент
entity Warehouse Склад
process System Система учёта заявок
Client --> System заявка
System --> Client статус
System --> Warehouse накладная
```
````
Ключевые слова узлов: `process`, `entity`, `store`, `control`, `channel`. Потоки: `-->`, `->>`, `<->`, сигналы `::>` / `<::`. Контекст: `style context`. Полный синтаксис — [документация Upstream](https://github.com/pbauermeister/dfd/blob/main/doc/README.md).
Декомпозиция (уровень 0):
````markdown
%dfd1 DFD уровня 0
```dfd
entity Client Клиент
entity Warehouse Склад
process Accept Принять заявку
process Register Зарегистрировать
process Ship Сформировать накладную
store Orders Заявки
Client --> Accept заявка
Accept --> Orders новая заявка
Orders --> Register данные заявки
Register --> Ship подтверждённая заявка
Ship --> Warehouse накладная
Ship --> Client уведомление
```
````
Зависимости: `data-flow-diagram` идёт с md2gost (Python ≥3.11). Без Graphviz офлайн DFD не нарисуется — либо кнопка «Скачать Graphviz» / `scripts/fetch_graphviz.py`, либо `--diagram-fallback remote` → kroki.io.
## Файл схем: поля
```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:**
1. Системный Chrome / Edge (через Playwright, ничего не качаем)
2. Playwright Chromium в `%LOCALAPPDATA%\md2gost\ms-playwright` — кнопка «Скачать headless Chromium» или `md2gost --install-chromium`
3. QuickJS + mermaid.js (`mermaidx`) — офлайн без браузера
4. Локальный Kroki → `https://kroki.io` (если `--diagram-fallback remote`)
**IDEF0:** сразу Pillow (PNG, при `--diagram-format svg` ещё SVG). Java/Kroki не используются.
**DFD:**
1. [data-flow-diagram](https://github.com/pbauermeister/dfd) компилирует DSL → DOT
2. Вшитый / кэш / PATH Graphviz (`dot`, для `style context` — `neato`)
3. Локальный Kroki → `https://kroki.io` при `--diagram-fallback remote`
Формат: `--diagram-format png` (по умолчанию) или `svg`. Масштаб растра PlantUML / локального Mermaid / IDEF0: `--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`.
Чтобы правка встроенной схемы не сбрасывалась при обновлении приложения, оставьте пустой `author` (GUI делает это сама при сохранении изменённой встроенной схемы).
+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: табы оглавления (считаются от ширины полосы), отступы списков, формулы из шаблона, цвет (всегда чёрный).
+187
View File
@@ -0,0 +1,187 @@
# Автотитульник
md2gost умеет **сам собрать титульный лист** из вшитого шаблона `TitleTemplate.docx`, подставив институт, кафедру, дисциплину, преподавателя, номер работы, ФИО и группу.
Готовый чужой DOCX титула (`--title` / «Настройки → Файлы → Титульный лист») по-прежнему работает и **полностью отключает** автогенерацию.
---
## Когда титул генерируется
Автотитул **по умолчанию выключен**. Включается явно:
- CLI: `--auto-title`
- GUI: галочка **«Автотитул»**
При включённом автотитуле он собирается, если одновременно:
1. **Не задан** явный путь к титульному DOCX (`--title` пуст, в GUI поле «Титульный лист» пустое).
2. Рядом с `.md` есть файл **`info_conv.yaml`** *или* в начале Markdown есть блок **\`\`\`title**.
Если автотитул выключен или нет ни yaml, ни блока `title` — титул не вставляется (готовый DOCX по `--title` по-прежнему можно передать отдельно).
ФИО и группа **обязательны** для генерации: из профиля GUI / `md2gost.user.json` или из `--student` / `--group`. Без них конвертация с `--auto-title` завершится ошибкой с подсказкой.
---
## Быстрый сценарий (GUI)
1. Первый запуск → окно **«Данные студента»**: введите **ФИО** и **группу**, сохраните.
Позже: **Настройки → Студент…**.
2. В папке с отчётом создайте `info_conv.yaml` (см. ниже).
3. В начале `.md` при необходимости добавьте блок \`\`\`title с номером работы и годом.
4. Включите галочку **«Автотитул»** на главном окне.
5. **Не** указывайте готовый титул в «Настройки → Файлы», если хотите автогенерацию.
6. Конвертируйте как обычно — перед телом отчёта появится титул.
Профиль студента хранится в `md2gost.user.json` рядом с приложением (рядом с exe или в текущей папке при `python -m md2gost`) — тот же каталог, что и `md2gost.schemes.json`. В том же файле блок `metadata` — свойства DOCX (автор/название/примечание); GUI: **Настройки → Метаданные…**, CLI: `--doc-*`. Это не поля титульного листа.
---
## `info_conv.yaml` (данные курса / кафедры)
Файл кладётся **в тот же каталог**, что и конвертируемый `.md`. Имя строго: `info_conv.yaml`.
Пример:
```yaml
Институт: Институт информационных технологий (ИИТ)
Кафедра: Кафедра цифровой трансформации (ЦТ)
Дисциплина: Проектирование архитектуры цифровой организации
Преподаватель: Иванов Иван Иванович
Должность: старший преподаватель, кафедра ЦТ
```
Формат — плоский список `ключ: значение` (UTF-8). Вложенный YAML и списки не нужны. Строки с `#` в начале — комментарии; хвост ` # …` в значении отрезается.
Один `info_conv.yaml` удобно держать на весь семестр/дисциплину и не копировать институт с кафедрой в каждый отчёт.
---
## Блок `title` в Markdown
Опциональный блок в **начале** файла (до `# *СОДЕРЖАНИЕ`), ограда языка `title`. Перед конвертацией он **вырезается** из тела отчёта и не попадает в DOCX как код.
Типичное использование — номер работы и год:
````markdown
```title
number: 1
year: 2026
```
# *СОДЕРЖАНИЕ
[TOC]
…
````
Можно переопределить любые академические поля прямо в блоке (они сильнее yaml):
````markdown
```title
number: 3
year: 2026
discipline: Другая формулировка дисциплины
teacher: Петров П. П.
```
````
Английские ключи тоже принимаются: `institute`, `department`, `discipline`, `teacher`, `teacher_position`, `number`, `year`.
---
## Поля: куда попадают и откуда берутся
| Поле на титуле | Ключи (любой из вариантов) | Источник |
|----------------|----------------------------|----------|
| Институт | `Институт`, `Institute`, `institute`; опечатка `Инститиут` | yaml / \`\`\`title |
| Кафедра | `Кафедра`, `Department`, `department` | yaml / \`\`\`title |
| Дисциплина (в «…») | `Дисциплина`, `Discipline`, `discipline` | yaml / \`\`\`title |
| № практической работы | `number`, `номер`, `№`, `n` | \`\`\`title (иначе `1`) |
| Группа («студент группы …») | — | профиль GUI / `--group` |
| ФИО студента | — | профиль GUI / `--student` |
| Должность преподавателя | `Должность`, `должность преподавателя`, `teacher_position` | yaml / \`\`\`title |
| ФИО преподавателя | `Преподаватель`, `Преподователь`, `Teacher`, `teacher` | yaml / \`\`\`title |
| Год («Москва … г.») | `year`, `год` | \`\`\`title (иначе текущий календарный год) |
Строка «ОТЧЁТ ПО ПРАКТИЧЕСКОЙ РАБОТЕ» и шапка МИРЭА в шаблоне **не** заполняются из переменных — это фиксированный текст шаблона.
ФИО/`группа` из yaml или \`\`\`title **игнорируются** (даже если написать `Студент:` / `Группа:`) — студент всегда из профиля или CLI.
---
## Приоритет слияния
```
Академические поля: info_conv.yaml < ```title (блок сильнее)
Студент / группа: md2gost.user.json < --student / --group
Явный титул DOCX: --title / путь в GUI → генератор не вызывается
```
`year` без значения → текущий год. `number` без значения → `1`.
---
## CLI
```bash
# автотитул выкл. по умолчанию — только тело отчёта
python -m md2gost report.md -o report.docx
# включить автотитул (yaml + профиль из md2gost.user.json)
python -m md2gost report.md -o report.docx --auto-title
# ФИО и группа на один прогон
python -m md2gost report.md --auto-title --student "Тарасов Игорь Алексеевич" --group ИНБО-31-23
# свой готовый титул — автогенерация не используется
python -m md2gost report.md --title title.docx
```
---
## Пример структуры папки
```
Отчет_практики/
info_conv.yaml ← институт, кафедра, дисциплина, препод
report_pr1.md ← в начале ```title с number/year
figures/
…
```
`report_pr1.md`:
````markdown
```title
number: 1
year: 2026
```
# *СОДЕРЖАНИЕ
[TOC]
# *ВВЕДЕНИЕ
…
````
---
## Частые вопросы
**Титул не появился.**
Проверьте: включён ли `--auto-title` / галочка «Автотитул»; нет ли пути в «Титульный лист» / `--title`; есть ли `info_conv.yaml` или \`\`\`title; заполнены ли ФИО и группа.
**Не тот номер работы.**
Задайте `number:` в \`\`\`title — из yaml номер не читается (только из блока или дефолт `1`).
**Нужен другой шаблон вёрстки.**
Сейчас слоты привязаны к bundled `TitleTemplate.docx`. Чужой макет — собирайте титул вручную и передавайте через `--title`.
**Задание (бланк).**
Автотитул не заменяет бланк задания: по-прежнему `--assignment` / «Бланк задания» в GUI.
**Где лежит профиль.**
`md2gost.user.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 Варианты развития архитектуры …
Источники: 7–20.
```bash
python -m md2gost report.md -o report.docx --type APID_coursework --check --title title.docx --assignment assignment.docx
```
+46
View File
@@ -0,0 +1,46 @@
# Импорт Word → Markdown (word2md)
Обратный пайплайн: **DOCX → Markdown** в диалекте md2gost. Удобно, если отчёт уже набран в Word (или собран md2gost раньше), а дальше править удобнее в `.md`.
Это **эвристический** импорт, не lossless round-trip: исходник UML/Mermaid, `%id` из исходного md и язык fence у листинга из DOCX не восстанавливаются.
## Запуск
```bash
python -m word2md report.docx -o report.md
word2md report.docx
python -m md2gost report.docx -o report.md # тот же импорт через md2gost
```
В GUI: перетащите `.docx` или кнопка **«Импорт DOCX→MD»**.
Картинки пишутся в каталог `<stem>_media/` рядом с `.md` (или `--media-dir`).
| Флаг | Смысл |
|------|--------|
| `-o` / `--output` | путь к `.md` |
| `--media-dir` | каталог картинок |
| `--keep-toc-pages` | не выкидывать строки содержания Word (по умолчанию только `[TOC]`) |
| `--pagebreaks` | писать `---` на разрывах страниц |
## Что распознаётся
- заголовки `Heading N` / спецразделы → `# *ВВЕДЕНИЕ`, `# 1 …`
- содержание → `# *СОДЕРЖАНИЕ` + `[TOC]` (поле TOC и строки `toc N` отбрасываются)
- подписи `Рисунок N — …`, `Таблица N`, `Листинг N` (+ «Продолжение…» склеивается)
- таблицы с merge → `^` / `>`
- листинги стиля `Code`
- формулы (таблица Formula / OMML) → `$$…$$`
- библиография `[1]: …`, ссылки `[1]` в тексте
- второй проход: `Рисунок 1.1` → `@Рисунок:fig1_1` (если номер известен по подписи)
Титул и бланк задания до первого H1/спецраздела пропускаются. Колонтитулы не импортируются.
## Ограничения
- Нет восстановления PlantUML / Mermaid (только PNG/JPEG из файла)
- OMML → формула — best-effort, не идеальный LaTeX
- Надписи, SmartArt, сноски, плавающие рисунки — пропуск с записью в лог
- Титул, свёрстанный большой таблицей, может попасть в поток, если walker уже «стартовал» — обычно титул до первого Heading пропускается
После импорта имеет смысл прогнать `python -m md2gost report.md --check`.
Binary file not shown.
+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
}
}
}
Binary file not shown.
Binary file not shown.
-860
View File
@@ -1,860 +0,0 @@
import docx.text.font as Font
from typing import Optional, Union, Dict, Any
from enum import Enum
class ThemeFont(Enum):
MAJOR_ASCII = "majorAscii"
MAJOR_H_ANSI = "majorHAnsi"
MAJOR_EAST_ASIAN = "majorEastAsia"
MAJOR_BIDI = "majorBidi"
MINOR_ASCII = "minorAscii"
MINOR_H_ANSI = "minorHAnsi"
MINOR_EAST_ASIAN = "minorEastAsia"
MINOR_BIDI = "minorBidi"
class Font:
"""
Класс, представляющий форматирование шрифта для пробега текста (run) в Word.
Поддерживает три-state значения (True/False/None) и множественные шрифты.
"""
def __init__(self):
# Основные шрифты
self._ascii: Optional[str] = None
self._h_ansi: Optional[str] = None
self._east_asia: Optional[str] = None
self._cs: Optional[str] = None
# Тематические шрифты (меньший приоритет)
self._ascii_theme: Optional[ThemeFont] = None
self._h_ansi_theme: Optional[ThemeFont] = None
self._east_asia_theme: Optional[ThemeFont] = None
self._cs_theme: Optional[ThemeFont] = None
# Три-state булевы свойства (None = унаследовано)
self._bold: Optional[bool] = None
self._italic: Optional[bool] = None
self._small_caps: Optional[bool] = None
self._strike: Optional[bool] = None
self._double_strike: Optional[bool] = None
self._outline: Optional[bool] = None
self._shadow: Optional[bool] = None
self._emboss: Optional[bool] = None
self._imprint: Optional[bool] = None
self._hidden: Optional[bool] = None
self._all_caps: Optional[bool] = None
self._complex_script: Optional[bool] = None
self._rtl: Optional[bool] = None
self._no_proof: Optional[bool] = None
# --- Свойство: name (основное имя шрифта) ---
@property
def name(self) -> Optional[str]:
"""Возвращает имя основного шрифта (w:ascii)."""
return self._ascii
@name.setter
def name(self, value: Optional[str]) -> None:
"""
Устанавливает w:ascii и w:hAnsi одновременно.
Если значение None — оба атрибута удаляются.
"""
if value is None:
self._ascii = None
self._h_ansi = None
else:
self._ascii = str(value)
self._h_ansi = str(value)
# --- Другие шрифты ---
@property
def east_asia(self) -> Optional[str]:
return self._east_asia
@east_asia.setter
def east_asia(self, value: Optional[str]) -> None:
self._east_asia = value
@property
def complex_script(self) -> Optional[str]:
return self._cs
@complex_script.setter
def complex_script(self, value: Optional[str]) -> None:
self._cs = value
@property
def ascii_theme(self) -> Optional[ThemeFont]:
return self._ascii_theme
@ascii_theme.setter
def ascii_theme(self, theme_font: Optional[ThemeFont]) -> None:
if isinstance(theme_font, ThemeFont) or theme_font is None:
self._ascii_theme = theme_font
else:
raise ValueError("ascii_theme must be a ThemeFont or None")
@property
def h_ansi_theme(self) -> Optional[ThemeFont]:
return self._h_ansi_theme
@h_ansi_theme.setter
def h_ansi_theme(self, theme_font: Optional[ThemeFont]) -> None:
self._h_ansi_theme = theme_font
# --- Три-state Boolean Properties (Toggle properties marked) ---
@property
def bold(self) -> Optional[bool]:
return self._bold
@bold.setter
def bold(self, value: Optional[bool]) -> None:
if value in (True, False, None):
self._bold = value
else:
raise ValueError("bold must be True, False, or None")
@property
def italic(self) -> Optional[bool]:
return self._italic
@italic.setter
def italic(self, value: Optional[bool]) -> None:
if value in (True, False, None):
self._italic = value
else:
raise ValueError("italic must be True, False, or None")
@property
def small_caps(self) -> Optional[bool]:
return self._small_caps
@small_caps.setter
def small_caps(self, value: Optional[bool]) -> None:
if value in (True, False, None):
self._small_caps = value
else:
raise ValueError("small_caps must be True, False, or None")
@property
def strike(self) -> Optional[bool]:
return self._strike
@strike.setter
def strike(self, value: Optional[bool]) -> None:
if value in (True, False, None):
self._strike = value
else:
raise ValueError("strike must be True, False, or None")
@property
def double_strike(self) -> Optional[bool]:
return self._double_strike
@double_strike.setter
def double_strike(self, value: Optional[bool]) -> None:
if value in (True, False, None):
self._double_strike = value
else:
raise ValueError("double_strike must be True, False, or None")
@property
def outline(self) -> Optional[bool]:
return self._outline
@outline.setter
def outline(self, value: Optional[bool]) -> None:
if value in (True, False, None):
self._outline = value
else:
raise ValueError("outline must be True, False, or None")
@property
def shadow(self) -> Optional[bool]:
return self._shadow
@shadow.setter
def shadow(self, value: Optional[bool]) -> None:
if value in (True, False, None):
self._shadow = value
else:
raise ValueError("shadow must be True, False, or None")
@property
def emboss(self) -> Optional[bool]:
return self._emboss
@emboss.setter
def emboss(self, value: Optional[bool]) -> None:
if value in (True, False, None):
self._emboss = value
else:
raise ValueError("emboss must be True, False, or None")
@property
def imprint(self) -> Optional[bool]:
return self._imprint
@imprint.setter
def imprint(self, value: Optional[bool]) -> None:
if value in (True, False, None):
self._imprint = value
else:
raise ValueError("imprint must be True, False, or None")
@property
def hidden(self) -> Optional[bool]:
return self._hidden
@hidden.setter
def hidden(self, value: Optional[bool]) -> None:
if value in (True, False, None):
self._hidden = value
else:
raise ValueError("hidden must be True, False, or None")
@property
def all_caps(self) -> Optional[bool]:
return self._all_caps
@all_caps.setter
def all_caps(self, value: Optional[bool]) -> None:
if value in (True, False, None):
self._all_caps = value
else:
raise ValueError("all_caps must be True, False, or None")
@property
def cs(self) -> Optional[bool]:
return self._complex_script
@cs.setter
def cs(self, value: Optional[bool]) -> None:
if value in (True, False, None):
self._complex_script = value
else:
raise ValueError("cs must be True, False, or None")
@property
def rtl(self) -> Optional[bool]:
return self._rtl
@rtl.setter
def rtl(self, value: Optional[bool]) -> None:
if value in (True, False, None):
self._rtl = value
else:
raise ValueError("rtl must be True, False, or None")
@property
def no_proof(self) -> Optional[bool]:
return self._no_proof
@no_proof.setter
def no_proof(self, value: Optional[bool]) -> None:
if value in (True, False, None):
self._no_proof = value
else:
raise ValueError("no_proof must be True, False, or None")
# --- Методы сериализации в XML / словарь ---
def to_xml_dict(self) -> Dict[str, Any]:
"""Преобразует текущее состояние шрифта в словарь, имитирующий XML."""
r_pr = {}
fonts = {}
if self._ascii:
fonts['ascii'] = self._ascii
if self._h_ansi:
fonts['hAnsi'] = self._h_ansi
if self._east_asia:
fonts['eastAsia'] = self._east_asia
if self._cs:
fonts['cs'] = self._cs
if self._ascii_theme:
fonts['asciiTheme'] = self._ascii_theme.value
if self._h_ansi_theme:
fonts['hAnsiTheme'] = self._h_ansi_theme.value
if self._east_asia_theme:
fonts['eastAsiaTheme'] = self._east_asia_theme.value
if self._cs_theme:
fonts['csTheme'] = self._cs_theme.value
if fonts:
r_pr['rFonts'] = fonts
# Boolean properties (если не None)
bool_map = {
'b': self._bold,
'i': self._italic,
'caps': self._all_caps,
'smallCaps': self._small_caps,
'strike': self._strike,
'dstrike': self._double_strike,
'outline': self._outline,
'shadow': self._shadow,
'emboss': self._emboss,
'imprint': self._imprint,
'vanish': self._hidden,
'cs': self._complex_script,
'rtl': self._rtl,
'noProof': self._no_proof
}
for tag, val in bool_map.items():
if val is not None:
r_pr[tag] = 'on' if val else 'off'
return r_pr
def __repr__(self) -> str:
return f"<Font name={self.name!r} bold={self.bold} italic={self.italic}>"
class FontManager:
"""
Централизованный менеджер шрифтов, использующий темы.
Может устанавливать глобальные шрифты (major/minor) для разных языковых групп.
"""
def __init__(self):
self._theme_fonts: Dict[ThemeFont, str] = {}
def set_theme_font(self, theme_type: ThemeFont, font_name: str) -> None:
"""Устанавливает шрифт для определённой темы."""
self._theme_fonts[theme_type] = font_name
def get_theme_font(self, theme_type: ThemeFont) -> Optional[str]:
"""Получает шрифт по типу темы."""
return self._theme_fonts.get(theme_type)
def apply_theme_to_font(self, font: Font) -> None:
"""Применяет тему к объекту Font, если явно не задан основной шрифт."""
# Только если w:ascii не задан, используем w:asciiTheme
if font._ascii is None and font._ascii_theme:
font._ascii = self.get_theme_font(font._ascii_theme)
if font._h_ansi is None and font._h_ansi_theme:
font._h_ansi = self.get_theme_font(font._h_ansi_theme)
if font._east_asia is None and font._east_asia_theme:
font._east_asia = self.get_theme_font(font._east_asia_theme)
if font._cs is None and font._cs_theme:
font._cs = self.get_theme_font(font._cs_theme)
def __repr__(self) -> str:
return f"<FontManager themes={len(self._theme_fonts)}>"
from enum import Enum
from typing import Optional, Dict, Any, List
class StyleType(Enum):
PARAGRAPH = "paragraph"
CHARACTER = "character"
TABLE = "table"
NUMBERING = "numbering"
class Style:
"""
Представление одного стиля Word (<w:style>).
Поддерживает атрибуты: имя, ID, тип, базовый стиль, приоритет, поведение и форматирование.
"""
def __init__(
self,
style_id: str,
type_: StyleType,
name: Optional[str] = None,
default: bool = False,
custom_style: bool = False,
):
self.style_id = style_id
self.type = type_
self.default = default
self.custom_style = custom_style
# Основные свойства
self._name: Optional[str] = name or style_id # fallback на styleId
self._based_on: Optional[str] = None # ссылка на другой styleId
self._next: Optional[str] = None
self._link: Optional[str] = None
# Поведенческие флаги
self._ui_priority: Optional[int] = None
self._semi_hidden: bool = False
self._unhide_when_used: bool = False
self._q_format: bool = False
self._locked: bool = False
# Форматирование
self._paragraph_format: Optional[Dict[str, Any]] = None
self._font: Optional[Dict[str, Any]] = None
self._table_properties: Optional[Dict[str, Any]] = None
self._row_properties: Optional[Dict[str, Any]] = None
self._cell_properties: Optional[Dict[str, Any]] = None
self._table_style_properties: List[Dict[str, Any]] = []
# Служебное
self._rsid: Optional[str] = None
# --- Свойства ---
@property
def name(self) -> str:
return self._name or self.style_id
@name.setter
def name(self, value: str) -> None:
self._name = value
@property
def based_on(self) -> Optional[str]:
return self._based_on
@based_on.setter
def based_on(self, style_id: Optional[str]) -> None:
self._based_on = style_id
@property
def next_style(self) -> Optional[str]:
return self._next
@next_style.setter
def next_style(self, style_id: Optional[str]) -> None:
self._next = style_id
@property
def linked_style(self) -> Optional[str]:
return self._link
@linked_style.setter
def linked_style(self, style_id: Optional[str]) -> None:
self._link = style_id
@property
def priority(self) -> Optional[int]:
return self._ui_priority
@priority.setter
def priority(self, value: Optional[int]) -> None:
if value is not None and not isinstance(value, int):
raise ValueError("priority must be an integer or None")
self._ui_priority = value
@property
def semi_hidden(self) -> bool:
return self._semi_hidden
@semi_hidden.setter
def semi_hidden(self, value: bool) -> None:
self._semi_hidden = bool(value)
@property
def unhide_when_used(self) -> bool:
return self._unhide_when_used
@unhide_when_used.setter
def unhide_when_used(self, value: bool) -> None:
self._unhide_when_used = bool(value)
@property
def quick_style(self) -> bool:
return self._q_format
@quick_style.setter
def quick_style(self, value: bool) -> None:
self._q_format = bool(value)
@property
def locked(self) -> bool:
return self._locked
@locked.setter
def locked(self, value: bool) -> None:
self._locked = bool(value)
# --- Форматирование ---
def set_paragraph_format(self, ppr: Dict[str, Any]) -> None:
self._paragraph_format = ppr
def set_font(self, rpr: Dict[str, Any]) -> None:
self._font = rpr
def set_table_properties(self, tbl_pr: Dict[str, Any]) -> None:
self._table_properties = tbl_pr
def set_row_properties(self, tr_pr: Dict[str, Any]) -> None:
self._row_properties = tr_pr
def set_cell_properties(self, tc_pr: Dict[str, Any]) -> None:
self._cell_properties = tc_pr
def add_table_style_property(self, prop: Dict[str, Any]) -> None:
self._table_style_properties.append(prop)
# --- Вспомогательные методы ---
def is_builtin(self) -> bool:
"""Возвращает True, если стиль встроенный (не пользовательский)."""
return not self.custom_style
def is_visible_in_gallery(self) -> bool:
"""Появляется ли стиль в галерее стилей?"""
return not self.semi_hidden and self.quick_style
def to_xml_dict(self) -> Dict[str, Any]:
"""Сериализация в словарь, близкий к XML."""
result = {
"w:type": self.type.value,
"w:styleId": self.style_id,
}
if self.default:
result["w:default"] = "1"
if self.custom_style:
result["w:customStyle"] = "1"
children = []
if self._name:
children.append({"w:name": {"w:val": self._name}})
if self._based_on:
children.append({"w:basedOn": {"w:val": self._based_on}})
if self._next:
children.append({"w:next": {"w:val": self._next}})
if self._link:
children.append({"w:link": {"w:val": self._link}})
if self._ui_priority is not None:
children.append({"w:uiPriority": {"w:val": str(self._ui_priority)}})
if self._semi_hidden:
children.append({"w:semiHidden": {}})
if self._unhide_when_used:
children.append({"w:unhideWhenUsed": {}})
if self._q_format:
children.append({"w:qFormat": {}})
if self._locked:
children.append({"w:locked": {}})
if self._rsid:
children.append({"w:rsid": {"w:val": self._rsid}})
if self._paragraph_format:
children.append({"w:pPr": self._paragraph_format})
if self._font:
children.append({"w:rPr": self._font})
if self._table_properties:
children.append({"w:tblPr": self._table_properties})
if self._row_properties:
children.append({"w:trPr": self._row_properties})
if self._cell_properties:
children.append({"w:tcPr": self._cell_properties})
for tsp in self._table_style_properties:
children.append({"w:tblStylePr": tsp})
if children:
result["_children"] = children
return result
def __repr__(self) -> str:
return f"<Style id='{self.style_id}' type={self.type.name} name='{self.name}'>"
from typing import Iterator, Optional, Dict, List
import re
class Styles:
"""
Коллекция всех стилей документа: определённых, встроенных и latent.
Имитирует поведение Word по сортировке, отображению и разрешению стилей.
"""
def __init__(self):
self._styles: Dict[str, Style] = {} # styleId -> Style
self._latent_styles: Dict[str, Dict[str, Any]] = {}
self._doc_defaults = {
"rPr": {},
"pPr": {}
}
self._latent_count = 0
self._def_semi_hidden = True
self._def_unhide_when_used = True
self._def_q_format = False
self._def_locked_state = False
self._def_ui_priority = 99
# --- Управление latent styles ---
def define_latent_style(
self,
name: str,
ui_priority: Optional[int] = None,
semi_hidden: Optional[bool] = None,
unhide_when_used: Optional[bool] = None,
q_format: Optional[bool] = None,
locked: Optional[bool] = None,
) -> None:
"""Добавляет исключение для latent style."""
self._latent_styles[name] = {
"uiPriority": ui_priority,
"semiHidden": semi_hidden,
"unhideWhenUsed": unhide_when_used,
"qFormat": q_format,
"locked": locked,
}
def set_latent_defaults(
self,
count: int = 276,
def_semi_hidden: bool = True,
def_unhide_when_used: bool = True,
def_q_format: bool = False,
def_locked_state: bool = False,
def_ui_priority: int = 99,
) -> None:
"""Устанавливает параметры по умолчанию для latent styles."""
self._latent_count = count
self._def_semi_hidden = def_semi_hidden
self._def_unhide_when_used = def_unhide_when_used
self._def_q_format = def_q_format
self._def_locked_state = def_locked_state
self._def_ui_priority = def_ui_priority
def get_latent_behavior(self, name: str) -> Dict[str, Any]:
"""Возвращает поведенческие атрибуты для встроенного стиля."""
base = {
"semiHidden": self._def_semi_hidden,
"unhideWhenUsed": self._def_unhide_when_used,
"qFormat": self._def_q_format,
"locked": self._def_locked_state,
"uiPriority": self._def_ui_priority,
}
override = self._latent_styles.get(name, {})
base.update({k: v for k, v in override.items() if v is not None})
return base
# --- Добавление/получение стилей ---
def add_style(
self,
style_id: str,
type_: StyleType,
name: Optional[str] = None,
default: bool = False,
custom_style: bool = False,
) -> Style:
"""Создаёт и добавляет новый стиль."""
style = Style(style_id, type_, name=name, default=default, custom_style=custom_style)
self._styles[style_id] = style
return style
def get_by_id(self, style_id: str) -> Optional[Style]:
"""Получить стиль по ID."""
return self._styles.get(style_id)
def get_by_name(self, name: str) -> Optional[Style]:
"""Найти стиль по имени (регистронезависимо)."""
for style in self._styles.values():
if style.name.lower() == name.lower():
return style
return None
def __getitem__(self, key: str) -> Style:
"""Доступ как по ID, так и по имени."""
style = self.get_by_id(key)
if style is None:
style = self.get_by_name(key)
if style is None:
raise KeyError(f"Style '{key}' not found.")
return style
def __setitem__(self, key: str, style: Style) -> None:
if not isinstance(style, Style):
raise TypeError("Only Style objects can be added.")
self._styles[style.style_id] = style
def __delitem__(self, key: str) -> None:
style = self.get_by_id(key) or self.get_by_name(key)
if style is None:
raise KeyError(f"Style '{key}' not found.")
del self._styles[style.style_id]
def __contains__(self, key: str) -> bool:
return (key in self._styles) or (self.get_by_name(key) is not None)
def __len__(self) -> int:
return len(self._styles)
def __iter__(self) -> Iterator[Style]:
"""Итерация по всем определённым стилям."""
return iter(self._styles.values())
# --- Document defaults ---
def set_default_run_properties(self, rpr: Dict[str, Any]) -> None:
self._doc_defaults["rPr"] = rpr
def set_default_paragraph_properties(self, ppr: Dict[str, Any]) -> None:
self._doc_defaults["pPr"] = ppr
def get_default_font(self) -> Dict[str, Any]:
return self._doc_defaults["rPr"]
def get_default_paragraph_format(self) -> Dict[str, Any]:
return self._doc_defaults["pPr"]
# --- Списки стилей (как в интерфейсе Word) ---
def recommended_list(self) -> List[Style]:
"""Стили, отображаемые в категории 'Recommended'."""
return [
s for s in self._styles.values()
if not s.semi_hidden
]
def styles_in_use(self, used_style_ids: List[str]) -> List[Style]:
"""Стили, применённые в документе и не semiHidden."""
return [
s for s in self._styles.values()
if s.style_id in used_style_ids and not s.semi_hidden
]
def in_current_document(self) -> List[Style]:
"""Все определённые стили, кроме semiHidden."""
return [s for s in self._styles.values() if not s.semi_hidden]
def all_styles(self) -> List[Style]:
"""Все определённые стили (включая semiHidden)."""
return list(self._styles.values())
# --- Сортировка ---
def sorted_by_appearance(self) -> List[Style]:
"""
Сортировка стилей так, как они отображаются в панели стилей Word:
- По uiPriority (возрастание)
- При равном приоритете — по имени (алфавитно)
- Custom-стили без uiPriority попадают в начало (uiPriority=0)
"""
def sort_key(style: Style) -> tuple:
prio = style.priority if style.priority is not None else 0
return (prio, style.name.lower())
return sorted(self._styles.values(), key=sort_key)
# --- Генерация XML ---
def to_xml_dict(self) -> Dict[str, Any]:
result = {
"w:styles": {
"_children": []
}
}
# docDefaults
if any(self._doc_defaults["rPr"]) or any(self._doc_defaults["pPr"]):
defaults = {"_children": []}
if self._doc_defaults["rPr"]:
defaults["_children"].append({
"w:rPrDefault": {
"_children": [{"w:rPr": self._doc_defaults["rPr"]}]
}
})
if self._doc_defaults["pPr"]:
defaults["_children"].append({
"w:pPrDefault": {
"_children": [{"w:pPr": self._doc_defaults["pPr"]}]
}
})
result["w:styles"]["_children"].append(defaults)
# latentStyles
latent_children = []
for name, props in self._latent_styles.items():
exc = {"w:name": name}
if props["uiPriority"] is not None:
exc["w:uiPriority"] = str(props["uiPriority"])
if props["semiHidden"] is not None:
exc["w:semiHidden"] = "1" if props["semiHidden"] else "0"
if props["unhideWhenUsed"] is not None:
exc["w:unhideWhenUsed"] = "1" if props["unhideWhenUsed"] else "0"
if props["qFormat"] is not None:
exc["w:qFormat"] = "1" if props["qFormat"] else "0"
if props["locked"] is not None:
exc["w:locked"] = "1" if props["locked"] else "0"
latent_children.append({"w:lsdException": exc})
latent_attrs = {
"w:count": str(self._latent_count),
"w:defSemiHidden": "1" if self._def_semi_hidden else "0",
"w:defUnhideWhenUsed": "1" if self._def_unhide_when_used else "0",
"w:defQFormat": "1" if self._def_q_format else "0",
"w:defLockedState": "1" if self._def_locked_state else "0",
"w:defUIPriority": str(self._def_ui_priority),
}
result["w:styles"]["_children"].append({
"w:latentStyles": {
"_children": latent_children,
**latent_attrs
}
})
# Все стили
for style in self._styles.values():
result["w:styles"]["_children"].append({"w:style": style.to_xml_dict()})
return result
def __repr__(self) -> str:
return f"<Styles count={len(self)} latent={len(self._latent_styles)}>"
# Создаём коллекцию стилей
styles = Styles()
# Настройка latent-стилей (встроенные по умолчанию)
styles.set_latent_defaults(count=276, def_semi_hidden=True, def_unhide_when_used=True)
styles.define_latent_style("Normal", ui_priority=0, semi_hidden=False, unhide_when_used=False, q_format=True)
styles.define_latent_style("Heading 1", ui_priority=9, q_format=True)
# Добавляем стили
normal = styles.add_style("Normal", StyleType.PARAGRAPH, name="Normal", default=True)
normal.quick_style = True
normal.semi_hidden = False
heading1 = styles.add_style("Heading1", StyleType.PARAGRAPH, name="Heading 1")
heading1.based_on = "Normal"
heading1.priority = 9
heading1.quick_style = True
heading1.semi_hidden = False
heading1.set_font({"b": {}, "sz": {"w:val": "28"}})
foobar = styles.add_style("Foobar", StyleType.PARAGRAPH, name="Foobar", custom_style=True)
foobar.priority = 50
foobar.semi_hidden = False
foobar.quick_style = True
# Установка умолчаний документа
styles.set_default_run_properties({
"rFonts": {"w:asciiTheme": "minorHAnsi"},
"sz": {"w:val": "24"}
})
styles.set_default_paragraph_properties({"spacing": {"w:after": "120"}})
# Получение стилей
print(styles["Normal"]) # <Style id='Normal' ...>
print(styles["Heading 1"]) # найдёт по имени
# Список рекомендованных
for s in styles.recommended_list():
print(s.name)
# Сортировка
for s in styles.sorted_by_appearance():
print(f"{s.name} (priority={s.priority})")
# Экспорт
xml_data = styles.to_xml_dict()
Binary file not shown.
-32
View File
@@ -1,32 +0,0 @@
# открытие файла
# установка нужных шрифтов в документе 1.
# работа с текстом
# работа с таблицами
# -поддержка переноса таблицы
# сохранение
# Настройка оглавлений
from docx import Document
# Создаем новый документ
document = Document()
# Добавляем заголовок
document.add_heading('Пример документа Word', level=1)
# Добавляем параграф с текстом
document.add_paragraph('Это первый параграф, который был добавлен с помощью Python.')
# Добавляем еще один параграф
document.add_paragraph('Это второй параграф. Можно добавлять новый текст, как вам нужно.')
# Добавляем текст в уже существующий параграф
paragraph = document.add_paragraph('А это текст, добавленный к ')
paragraph.add_run('существующему ').italic = True
paragraph.add_run('параграфу.')
# Сохраняем документ
document.save('мой_документ.docx')
print("Документ 'мой_документ.docx' успешно создан и текст добавлен.")
-915
View File
@@ -1,915 +0,0 @@
from abc import ABC, abstractmethod
from typing import Dict, List, Any, Optional, Union, Callable
from docx import Document
from docx.shared import Pt, Cm, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING
from docx.enum.style import WD_STYLE_TYPE
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
import re
class GostDocumentConfig:
"""Конфигурация параметров документа по ГОСТ"""
def __init__(self, **kwargs):
# Поля документа (в сантиметрах)
self.margins = kwargs.get('margins', {
'top': 2.0,
'bottom': 2.0,
'left': 3.0,
'right': 1.5
})
# Шрифты
self.font = kwargs.get('font', {
'name': 'Times New Roman',
'size': 14,
'main_language': 'ru-RU'
})
# Межстрочный интервал
self.line_spacing = kwargs.get('line_spacing', 1.5)
# Отступы для заголовков
self.heading_spacing = kwargs.get('heading_spacing', {
1: {'before': 24, 'after': 12},
2: {'before': 24, 'after': 6},
3: {'before': 18, 'after': 6}
})
# Стили заголовков
self.heading_styles = kwargs.get('heading_styles', {
1: {'size': 16, 'bold': True, 'all_caps': True, 'align': 'center'},
2: {'size': 14, 'bold': True, 'all_caps': False, 'align': 'left'},
3: {'size': 14, 'bold': True, 'italic': True, 'all_caps': False, 'align': 'left'}
})
# Стиль абзаца
self.paragraph = kwargs.get('paragraph', {
'first_line_indent': 1.25, # в см
'alignment': 'justify'
})
# Стиль таблиц
self.table = kwargs.get('table', {
'font_size': 12,
'header_bold': True
})
# Стиль подписей
self.caption = kwargs.get('caption', {
'font_size': 12,
'italic': True,
'align': 'center'
})
class GostDocumentCore(ABC):
"""Базовый класс для создания документов по ГОСТ с возможностью расширения"""
def __init__(self, config: Optional[GostDocumentConfig] = None,
title: str = "", author: str = ""):
"""
Инициализация документа с настройками ГОСТ
Args:
config: Конфигурация документа (если None - используются стандартные настройки ГОСТ)
title: Заголовок документа
author: Автор документа
"""
self.config = config or GostDocumentConfig()
self.title = title
self.author = author
self.document = Document()
self._element_processors = {}
self._setup_document()
self._create_gost_styles()
# Регистрируем базовые процессоры элементов
self.register_processor('heading', self._process_heading)
self.register_processor('paragraph', self._process_paragraph)
self.register_processor('table', self._process_table)
self.register_processor('list', self._process_list)
self.register_processor('image', self._process_image)
def _setup_document(self):
"""Настройка базовых параметров документа согласно ГОСТ"""
# Настройка полей страницы
sections = self.document.sections
for section in sections:
section.top_margin = Cm(self.config.margins['top'])
section.bottom_margin = Cm(self.config.margins['bottom'])
section.left_margin = Cm(self.config.margins['left'])
section.right_margin = Cm(self.config.margins['right'])
# Установка стандартного стиля для всего документа
style = self.document.styles['Normal']
font = style.font
font.name = self.config.font['name']
font.size = Pt(self.config.font['size'])
# Установка кириллического шрифта
font.element.rPr.rFonts.set(qn('w:eastAsia'), self.config.font['name'])
# Настройка формата абзаца
paragraph_format = style.paragraph_format
paragraph_format.line_spacing_rule = (
WD_LINE_SPACING.ONE_POINT_FIVE if self.config.line_spacing == 1.5
else WD_LINE_SPACING.DOUBLE if self.config.line_spacing == 2.0
else WD_LINE_SPACING.EXACTLY
)
paragraph_format.alignment = (
WD_ALIGN_PARAGRAPH.JUSTIFY if self.config.paragraph['alignment'] == 'justify'
else WD_ALIGN_PARAGRAPH.LEFT if self.config.paragraph['alignment'] == 'left'
else WD_ALIGN_PARAGRAPH.CENTER
)
paragraph_format.first_line_indent = Cm(self.config.paragraph['first_line_indent'])
def _create_gost_styles(self):
"""Создание стилей заголовков и других элементов в соответствии с ГОСТ"""
# Заголовок 1 уровня
self._create_heading_style(1)
# Заголовок 2 уровня
self._create_heading_style(2)
# Заголовок 3 уровня
self._create_heading_style(3)
# Стиль для списка
if 'List GOST' not in self.document.styles:
list_style = self.document.styles.add_style('List GOST', WD_STYLE_TYPE.PARAGRAPH)
list_style.base_style = self.document.styles['List']
list_style.font.name = self.config.font['name']
list_style.font.size = Pt(self.config.font['size'])
list_style.paragraph_format.left_indent = Cm(1.25)
list_style.paragraph_format.space_before = Pt(0)
list_style.paragraph_format.space_after = Pt(6)
list_style.paragraph_format.line_spacing_rule = (
WD_LINE_SPACING.ONE_POINT_FIVE if self.config.line_spacing == 1.5
else WD_LINE_SPACING.DOUBLE
)
# Стиль для подписей таблиц и рисунков
if 'Caption GOST' not in self.document.styles:
caption_style = self.document.styles.add_style('Caption GOST', WD_STYLE_TYPE.PARAGRAPH)
caption_style.base_style = self.document.styles['Caption']
caption_style.font.name = self.config.font['name']
caption_style.font.size = Pt(self.config.caption['font_size'])
caption_style.font.italic = self.config.caption['italic']
caption_style.paragraph_format.alignment = (
WD_ALIGN_PARAGRAPH.CENTER if self.config.caption['align'] == 'center'
else WD_ALIGN_PARAGRAPH.LEFT
)
caption_style.paragraph_format.space_before = Pt(6)
caption_style.paragraph_format.space_after = Pt(12)
def _create_heading_style(self, level: int):
"""Создает стиль заголовка заданного уровня"""
style_name = f'Heading {level} GOST'
if style_name in self.document.styles:
return
style = self.document.styles.add_style(style_name, WD_STYLE_TYPE.PARAGRAPH)
# Настройка шрифта
heading_config = self.config.heading_styles[level]
style.font.name = self.config.font['name']
style.font.size = Pt(heading_config['size'])
style.font.bold = heading_config['bold']
style.font.italic = heading_config.get('italic', False)
style.font.all_caps = heading_config['all_caps']
# Настройка абзаца
spacing = self.config.heading_spacing[level]
style.paragraph_format.space_before = Pt(spacing['before'])
style.paragraph_format.space_after = Pt(spacing['after'])
style.paragraph_format.line_spacing_rule = (
WD_LINE_SPACING.ONE_POINT_FIVE if self.config.line_spacing == 1.5
else WD_LINE_SPACING.DOUBLE
)
# Выравнивание
if heading_config['align'] == 'center':
style.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
elif heading_config['align'] == 'left':
style.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.LEFT
style.paragraph_format.first_line_indent = Cm(0) # Без красной строки для заголовков
def register_processor(self, element_type: str, processor: Callable):
"""
Регистрирует обработчик для определенного типа элемента
Args:
element_type: Тип элемента (например, 'heading', 'table')
processor: Функция-обработчик
"""
self._element_processors[element_type] = processor
def get_processor(self, element_type: str) -> Optional[Callable]:
"""
Возвращает обработчик для указанного типа элемента
Args:
element_type: Тип элемента
Returns:
Обработчик или None, если не найден
"""
return self._element_processors.get(element_type)
def _process_heading(self, element: Dict[str, Any]) -> None:
"""Обрабатывает элемент заголовка"""
level = element.get('level', 1)
text = element.get('text', '')
heading = self.document.add_heading(level=0)
style_name = f'Heading {level} GOST'
if style_name in self.document.styles:
heading.style = style_name
else:
# Базовая настройка, если стиль не создан
heading.alignment = WD_ALIGN_PARAGRAPH.CENTER if level == 1 else WD_ALIGN_PARAGRAPH.LEFT
run = heading.add_run(text)
run.font.size = Pt(16 if level == 1 else 14)
run.font.bold = True
# Добавляем текст
heading.add_run(text)
def _process_paragraph(self, element: Dict[str, Any]) -> None:
"""Обрабатывает элемент абзаца"""
text = element.get('text', '')
style = element.get('style')
p = self.document.add_paragraph()
if style and style in self.document.styles:
p.style = style
# Основной текст по ГОСТ
p.paragraph_format.line_spacing_rule = (
WD_LINE_SPACING.ONE_POINT_FIVE if self.config.line_spacing == 1.5
else WD_LINE_SPACING.DOUBLE
)
p.paragraph_format.alignment = (
WD_ALIGN_PARAGRAPH.JUSTIFY if self.config.paragraph['alignment'] == 'justify'
else WD_ALIGN_PARAGRAPH.LEFT
)
p.paragraph_format.first_line_indent = Cm(self.config.paragraph['first_line_indent'])
print(text)
# Добавляем текст
p.add_run(text)
def _process_table(self, element: Dict[str, Any]) -> None:
"""Обрабатывает элемент таблицы"""
data = element.get('data', [])
headers = element.get('headers')
# Добавляем подпись таблицы
caption = self.document.add_paragraph()
caption.style = 'Caption GOST'
caption.add_run("Таблица ").bold = True
# Создаем таблицу
rows = len(data) + (1 if headers else 0)
cols = len(data[0]) if data else 0
if cols == 0:
return
table = self.document.add_table(rows=rows, cols=cols)
table.style = 'Table Grid'
# Заполняем заголовки, если они есть
if headers:
hdr_cells = table.rows[0].cells
for i, header in enumerate(headers):
hdr_cells[i].text = header
for paragraph in hdr_cells[i].paragraphs:
for run in paragraph.runs:
run.font.bold = self.config.table['header_bold']
# Заполняем данные
start_row = 1 if headers else 0
for i, row_data in enumerate(data):
row_cells = table.rows[i + start_row].cells
for j, cell_data in enumerate(row_data):
row_cells[j].text = str(cell_data)
# Настройка шрифта для всех ячеек
for row in table.rows:
for cell in row.cells:
for paragraph in cell.paragraphs:
for run in paragraph.runs:
run.font.name = self.config.font['name']
run.font.size = Pt(self.config.table['font_size'])
def _process_list(self, element: Dict[str, Any]) -> None:
"""Обрабатывает элемент списка"""
items = element.get('items', [])
list_type = element.get('type', 'unordered') # 'unordered' или 'ordered'
for item in items:
p = self.document.add_paragraph(style='List GOST')
p.paragraph_format.left_indent = Cm(1.25)
p.paragraph_format.first_line_indent = Cm(-0.63) if list_type == 'ordered' else Cm(0)
if list_type == 'ordered':
# Добавляем номер вручную для упорядоченного списка
p.add_run(f"{items.index(item) + 1}. ").bold = True
p.add_run(item)
def _process_image(self, element: Dict[str, Any]) -> None:
"""Обрабатывает элемент изображения (заглушка для будущей реализации)"""
# В реальной реализации здесь будет добавление изображения
path = element.get('path', '')
width = element.get('width', None)
# Добавляем подпись
caption = self.document.add_paragraph()
caption.style = 'Caption GOST'
caption.add_run("Рисунок ").bold = True
# В реальной реализации здесь будет добавление изображения
# if path and os.path.exists(path):
# if width:
# self.document.add_picture(path, width=Cm(width))
# else:
# self.document.add_picture(path)
@abstractmethod
def add_element(self, element_type: str, **kwargs) -> None:
"""
Абстрактный метод для добавления элемента в документ.
Должен быть реализован в дочерних классах.
Args:
element_type: Тип элемента (heading, paragraph, table и т.д.)
**kwargs: Параметры элемента
"""
pass
def save(self, filename: str) -> None:
"""
Сохраняет документ в файл
Args:
filename: Имя файла для сохранения
"""
# Добавляем настройки совместимости
try:
self.document.settings.element.xpath('./w:compat')[0].clear()
except (IndexError, AttributeError):
pass
self.document.save(filename)
def create_title_page(self) -> None:
"""Создает титульную страницу документа"""
# Добавляем отступ сверху
for _ in range(5):
self.document.add_paragraph()
# Заголовок
title_paragraph = self.document.add_heading(level=0)
title_paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
title_run = title_paragraph.add_run(self.title)
title_run.font.size = Pt(16)
title_run.font.bold = True
title_run.font.name = self.config.font['name']
# Добавляем отступ
self.document.add_paragraph()
# Автор
author_paragraph = self.document.add_paragraph()
author_paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
author_run = author_paragraph.add_run(self.author)
author_run.font.size = Pt(self.config.font['size'])
author_run.font.name = self.config.font['name']
# Добавляем отступ снизу
for _ in range(10):
self.document.add_paragraph()
# Дата
date_paragraph = self.document.add_paragraph()
date_paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
date_run = date_paragraph.add_run("2023")
date_run.font.size = Pt(self.config.font['size'])
date_run.font.name = self.config.font['name']
# Устанавливаем титульную страницу как отдельный раздел
self.document.sections[0].start_type = 2 # Новая страница
class MarkdownGostDocument(GostDocumentCore):
"""
Расширенная реализация для обработки Markdown и преобразования в документ по ГОСТ
"""
def __init__(self, config: Optional[GostDocumentConfig] = None,
title: str = "", author: str = ""):
super().__init__(config, title, author)
self._markdown_processors = {}
self._setup_markdown_processors()
def _setup_markdown_processors(self):
"""Настраивает обработчики для различных элементов Markdown"""
# Заголовки
self.register_markdown_processor(r'#{1,6}\s+(.+)', self._process_md_heading)
# Обычный текст
self.register_markdown_processor(r'^(?!#{1,6}\s+)(?!-{3,})(?!#{1,6}$)(?!```)(?!!\[.*\]\(.*\)).+',
self._process_md_paragraph)
# Горизонтальная линия
self.register_markdown_processor(r'^-{3,}$', self._process_md_horizontal_rule)
# Списки
self.register_markdown_processor(r'^(\*|\-|\d+\.)\s+(.+)$', self._process_md_list)
# Таблицы (упрощенная обработка)
self.register_markdown_processor(r'^\|.*\|$', self._process_md_table)
# Изображения
self.register_markdown_processor(r'^!\[(.*)\]\((.*)\)$', self._process_md_image)
# Код
self.register_markdown_processor(r'^```(.*)$', self._process_md_code)
def register_markdown_processor(self, pattern: str, processor: Callable):
"""
Регистрирует обработчик для определенного паттерна Markdown
Args:
pattern: Регулярное выражение для паттерна
processor: Функция-обработчик
"""
self._markdown_processors[pattern] = processor
def _process_md_heading(self, match: re.Match, line: str) -> Dict[str, Any]:
"""Обрабатывает заголовок Markdown"""
heading_text = match.group(1).strip()
level = line.count('#')
# Ограничиваем уровень заголовка от 1 до 3 (согласно ГОСТ)
level = min(max(level, 1), 3)
return {'type': 'heading', 'level': level, 'text': heading_text}
def _process_md_paragraph(self, match: re.Match, line: str) -> Dict[str, Any]:
"""Обрабатывает обычный текст Markdown"""
return {'type': 'paragraph', 'text': line.strip()}
def _process_md_horizontal_rule(self, match: re.Match, line: str) -> Dict[str, Any]:
"""Обрабатывает горизонтальную линию Markdown"""
# В ГОСТ горизонтальные линии не используются, поэтому просто пропускаем
return None
def _process_md_list(self, match: re.Match, line: str) -> Dict[str, Any]:
"""Обрабатывает список Markdown"""
items = [line.strip()]
list_type = 'ordered' if re.match(r'^\d+\.', line) else 'unordered'
return {'type': 'list', 'items': items, 'type': list_type}
def _process_md_table(self, match: re.Match, line: str) -> Dict[str, Any]:
"""Обрабатывает таблицу Markdown (заглушка)"""
# Полная обработка таблиц требует анализа нескольких строк
return None # Будет обработано в полной реализации
def _process_md_image(self, match: re.Match, line: str) -> Dict[str, Any]:
"""Обрабатывает изображение Markdown"""
alt_text = match.group(1)
image_path = match.group(2)
return {'type': 'image', 'path': image_path, 'alt': alt_text}
def _process_md_code(self, match: re.Match, line: str) -> Dict[str, Any]:
"""Обрабатывает блок кода Markdown"""
# Код в ГОСТ обычно оформляется как отдельный раздел или сноска
return {'type': 'paragraph', 'text': f"Код: {line}", 'style': 'Code'}
def add_element(self, element_type: str, **kwargs) -> None:
"""
Добавляет элемент в документ согласно ГОСТ
Args:
element_type: Тип элемента (heading, paragraph, table и т.д.)
**kwargs: Параметры элемента
"""
processor = self.get_processor(element_type)
if processor:
element = {'type': element_type, **kwargs}
processor(element)
def process_markdown(self, markdown_text: str) -> None:
"""
Обрабатывает Markdown текст и добавляет элементы в документ
Args:
markdown_text: Текст в формате Markdown
"""
lines = markdown_text.split('\n')
i = 0
while i < len(lines):
line = lines[i].strip()
if not line:
i += 1
continue
processed = False
for pattern, processor in self._markdown_processors.items():
match = re.match(pattern, line)
if match:
element = processor(match, line)
if element:
self.add_element(element['type'], **{k: v for k, v in element.items() if k != 'type'})
processed = True
break
if not processed:
# Если строка не соответствует ни одному паттерну, обрабатываем как обычный текст
self.add_element('paragraph', text=line)
i += 1
def process_markdown_file(self, file_path: str) -> None:
"""
Обрабатывает файл Markdown и добавляет элементы в документ
Args:
file_path: Путь к файлу Markdown
"""
with open(file_path, 'r', encoding='utf-8') as f:
markdown_text = f.read()
self.process_markdown(markdown_text)
# Пример использования
if __name__ == "__main__":
# Создаем кастомную конфигурацию (можно оставить стандартную)
custom_config = GostDocumentConfig(
margins={
'top': 2.0,
'bottom': 2.0,
'left': 3.0,
'right': 1.5
},
font={
'name': 'Times New Roman',
'size': 14
},
line_spacing=1.5,
heading_spacing={
1: {'before': 24, 'after': 12},
2: {'before': 24, 'after': 6},
3: {'before': 18, 'after': 6}
},
heading_styles={
1: {'size': 16, 'bold': True, 'all_caps': True, 'align': 'center'},
2: {'size': 14, 'bold': True, 'all_caps': False, 'align': 'left'},
3: {'size': 14, 'bold': True, 'italic': True, 'all_caps': False, 'align': 'left'}
}
)
# Создаем документ с поддержкой Markdown
doc = MarkdownGostDocument(
config=custom_config,
title="Отчет о научно-исследовательской работе",
author="Иванов И.И., группа 123"
)
# Добавляем титульную страницу
doc.create_title_page()
# Пример обработки Markdown
markdown_example = """
## Практическая работа 6 (часть 2)
**Тема. Нелинейные структуры данных. Бинарное дерево.**
---
### 1. Условие задания и варианта
**Вариант 27**
**Значение информационной части:** Содержит символьное значение и счетчик (подсчитывает, сколько раз в дерево вставлялся символ).
**Операции варианта:**
1. Модифицировать алгоритм вставки ключа в дерево в соответствии со структурой узла.
2. Определить символ, который встретился большее количество раз.
3. Определить количество цифр в дереве, используя алгоритм обхода в ширину.
---
### 2. Свойства дерева варианта
- **Тип дерева:** Бинарное дерево поиска (БДП) без операции удаления.
- **Структура узла:**
```cpp
struct Node {
char symbol; // Символ
int count; // Счетчик вхождений
Node* left; // Указатель на левое поддерево
Node* right; // Указатель на правое поддерево
};
```
- **Особенности:**
- При вставке символа:
- Если символ уже существует в дереве, увеличивается его `count`.
- Иначе создается новый узел с `count = 1`.
- Упорядочение узлов: По ASCII-коду символов (меньшие символы — слева, большие — справа).
- Операция удаления не реализована (требования варианта 24–30).
---
### 3. Тестовые примеры
#### Пример 1: Вставка символов `a`, `a`, `b`, `3`, `3`, `3`
**Структура дерева:**
```
b(1)
a(2)
3(3)
```
**Пояснение:**
- `a` вставлен 2 раза → `count = 2`.
- `3` вставлен 3 раза → `count = 3` (самый частый символ).
- Количество цифр: 3 (только символ `3`).
#### Пример 2: Вставка символов `5`, `5`, `a`, `5`
**Структура дерева:**
```
a(1)
5(3)
```
**Пояснение:**
- `5` вставлен 3 раза → `count = 3` (самый частый символ).
- Количество цифр: 3.
---
### 4. Описание функций
#### 4.1. Модифицированная вставка символа
**Задача:** Добавить символ в БДП, увеличивая счетчик при совпадении.
**Предусловие:** Дерево инициализировано.
**Постусловие:** Символ добавлен или обновлен в дереве.
**Прототип:**
```cpp
void BinaryTree::insert(char symbol);
```
**Алгоритм:**
1. Начать с корня.
2. Пока текущий узел не `nullptr`:
- Если символ совпадает с текущим узлом → увеличить `count` и завершить.
- Если символ меньше текущего → перейти в левое поддерево.
- Иначе → перейти в правое поддерево.
3. Если узел не найден → создать новый узел с `count = 1`.
---
#### 4.2. Поиск самого частого символа
**Задача:** Найти символ с максимальным значением `count`.
**Предусловие:** Дерево не пустое.
**Постусловие:** Возвращен символ и его счетчик.
**Прототип:**
```cpp
std::pair<char, int> BinaryTree::findMostFrequent();
```
**Алгоритм:**
1. Инициализировать `maxCount = 0`, `maxSymbol = '\\0'`.
2. Обойти все узлы (инордер-обход):
- Для каждого узла сравнить `count` с `maxCount`.
- Если `count > maxCount` → обновить `maxSymbol` и `maxCount`.
3. Вернуть пару `(maxSymbol, maxCount)`.
---
#### 4.3. Подсчет цифр в дереве (обход в ширину)
**Задача:** Посчитать общее количество вхождений цифровых символов (`0`–`9`).
**Предусловие:** Дерево инициализировано.
**Постусловие:** Возвращено число — сумма `count` для всех цифровых символов.
**Прототип:**
```cpp
int BinaryTree::countDigits();
```
**Алгоритм:**
1. Инициализировать очередь и счетчик `total = 0`.
2. Поместить корень в очередь.
3. Пока очередь не пуста:
- Извлечь узел из очереди.
- Если символ узла — цифра (`'0'`–`'9'`) → добавить `count` к `total`.
- Поместить левый и правый дочерние узлы в очередь.
4. Вернуть `total`.
---
#### 4.4. Отображение дерева
**Задача:** Вывести структуру дерева на экран (повернутое на 90°).
**Предусловие:** Дерево инициализировано.
**Постусловие:** Дерево отображено в консоли.
**Прототип:**
```cpp
void BinaryTree::displayTree();
void BinaryTree::printTree(Node* node, int space);
```
**Алгоритм:**
1. Рекурсивно обойти дерево в порядке: **правое поддерево → корень → левое поддерево**.
2. Для каждого уровня увеличивать отступ (на 10 пробелов).
3. Вывести символ и `count` с текущим отступом.
---
### 5. Код программы
```cpp
#include <iostream>
#include <stack>
#include <queue>
using namespace std;
struct Node {
char symbol;
int count;
Node* left;
Node* right;
Node(char s) : symbol(s), count(1), left(nullptr), right(nullptr) {}
};
class BinaryTree {
private:
Node* root;
void printTree(Node* node, int space) {
if (node == nullptr) return;
space += 10;
printTree(node->right, space);
cout << endl;
for (int i = 10; i < space; i++) cout << " ";
cout << node->symbol << "(" << node->count << ")";
printTree(node->left, space);
}
public:
BinaryTree() : root(nullptr) {}
void insert(char symbol) {
Node** current = &root;
while (*current != nullptr) {
if (symbol == (*current)->symbol) {
(*current)->count++;
return;
} else if (symbol < (*current)->symbol) {
current = &((*current)->left);
} else {
current = &((*current)->right);
}
}
*current = new Node(symbol);
}
pair<char, int> findMostFrequent() {
if (root == nullptr) return make_pair('\\0', 0);
char maxSymbol = '\\0';
int maxCount = 0;
stack<Node*> s;
Node* current = root;
while (current != nullptr || !s.empty()) {
while (current != nullptr) {
s.push(current);
current = current->left;
}
current = s.top();
s.pop();
if (current->count > maxCount) {
maxCount = current->count;
maxSymbol = current->symbol;
}
current = current->right;
}
return make_pair(maxSymbol, maxCount);
}
int countDigits() {
if (root == nullptr) return 0;
int total = 0;
queue<Node*> q;
q.push(root);
while (!q.empty()) {
Node* current = q.front();
q.pop();
if (current->symbol >= '0' && current->symbol <= '9') {
total += current->count;
}
if (current->left) q.push(current->left);
if (current->right) q.push(current->right);
}
return total;
}
void displayTree() {
printTree(root, 0);
cout << "\n\n";
}
};
int main() {
BinaryTree tree;
int choice;
char symbol;
while (true) {
cout << "\n1. Insert symbol\n";
cout << "2. Display tree\n";
cout << "3. Find most frequent symbol\n";
cout << "4. Count digits\n";
cout << "5. Exit\n";
cout << "Enter choice: ";
cin >> choice;
switch (choice) {
case 1:
cout << "Enter symbol: ";
cin >> symbol;
tree.insert(symbol);
break;
case 2:
cout << "Tree structure:\n";
tree.displayTree();
break;
case 3: {
auto result = tree.findMostFrequent();
if (result.second == 0) {
cout << "Tree is empty.\n";
} else {
cout << "Most frequent symbol: " << result.first
<< " (count: " << result.second << ")\n";
}
break;
}
case 4:
cout << "Number of digits: " << tree.countDigits() << endl;
break;
case 5:
return 0;
default:
cout << "Invalid choice.\n";
}
}
}
```
---
### 6. Скриншоты тестирования
#### Тест 1: Вставка `a`, `a`, `b`, `3`, `3`, `3`
![Тест 1](https://i.imgur.com/1aXJz9l.png)
- **Вывод дерева:** Корректно отображена структура с `3(3)` как самым частым символом.
- **Количество цифр:** 3 (символ `3` вставлен 3 раза).
#### Тест 2: Вставка `5`, `5`, `a`, `5`
![Тест 2](https://i.imgur.com/5GkR7yP.png)
- **Самый частый символ:** `5` (count=3).
- **Количество цифр:** 3.
---
### 7. Ответы на контрольные вопросы
1. **Степень дерева** определяется максимальным количеством дочерних узлов у любого узла.
2. **Сильноветвящееся дерево** — дерево, где степень каждого узла равна максимальной степени дерева.
3. **Путь в дереве** — последовательность узлов от корня к целевому узлу.
4. **Длина пути** — количество ребер в пути (на 1 меньше количества узлов).
5. **Степень бинарного дерева** равна 2 (максимум 2 дочерних узла на узел).
6. **Пустое дерево** допустимо (корень = `nullptr`).
7. **Бинарное дерево** — дерево, где каждый узел имеет не более 2 дочерних узлов.
8. **Алгоритм обхода** используется для последовательного доступа ко всем узлам.
9. **Высота дерева:**
```
height(nullptr) = -1
height(node) = 1 + max(height(node->left), height(node->right))
```
10. **Дерево по таблице:**
![Дерево](https://i.imgur.com/mVcRf0D.png)
11. **Порядки обхода:**
- Прямой (КЛП): 18 → 12 → 7 → 4 → 10 → 2 → 5 → 21 → 15 → 14
- Симметричный (ЛКП): 7 → 12 → 2 → 10 → 5 → 18 → 21 → 4 → 14 → 15
- Обратный (ЛПК): 7 → 2 → 5 → 10 → 12 → 21 → 4 → 15 → 14 → 18
12. **Обход в ширину** использует **очередь**.
13. **Пример обхода в ширину:** 18 → 12 → 14 → 7 → 10 → 15 → 4 → 2 → 5 → 21.
14. **Не рекурсивный обход в глубину** использует **стек**.
15. **Обход дерева выражения `(5+3)*((1+2)/3)`:**
- Прямой: `* + 5 3 / + 1 2 3`
- Симметричный: `5 + 3 * 1 + 2 / 3`
- Обратный: `5 3 + 1 2 + 3 / *`
16. **Деревья выражений:**
- `a+b-c*d+e` → `+ - + a b * c d e`
- `/a-b*c d` → Ошибка в выражении (некорректный формат).
- `a b c d / - *` → `* a - b / c d`
- `* - / + a b c d e` → `* - / + a b c d e`
17. **Обход в ширину со стеком** даст порядок, аналогичный **обратному обходу в глубину**.
18. **БДП для симметричного обхода `40 45 46 50 65 70 75`:**
![БДП](https://i.imgur.com/3kLmF9A.png)
19. **Исходное дерево для прямого обхода `50 45 35 15 40 46 65 75 70`:**
![Дерево](https://i.imgur.com/8W0jKQv.png)
20. **БДП после вставки `1, 48, 75, 100`:**
![Дерево](https://i.imgur.com/5GkR7yP.png)
---
**Вывод:** Реализованы все операции для бинарного дерева поиска с модифицированной структурой узла. Программа корректно обрабатывает вставку, поиск самого частого символа и подсчет цифр через обход в ширину. Тесты подтверждают работоспособность алгоритмов..
"""
# Обрабатываем Markdown
doc.process_markdown(markdown_example)
# Сохраняем документ
doc.save("./asdotchet_po_GOST.docx")
print("Документ успешно создан и сохранен как 'otchet_po_GOST.docx'")
+102 -151
View File
@@ -11,13 +11,23 @@ from pathlib import Path
from md2gost.extended_markdown import markdown from md2gost.extended_markdown import markdown
from md2gost.diagram_renderer import DIAGRAM_LANGS from md2gost.diagram_renderer import DIAGRAM_LANGS
from md2gost.profiles import preprocess_markdown from md2gost.profiles import preprocess_markdown
from mdexport import (
BlockVisitor,
InlineRenderer,
caption_tuple,
is_contents_heading,
parse_image_caption,
reference_full_name,
render_inline,
walk_blocks,
)
from .escape import escape_text, xml_id from .escape import escape_text, xml_id
from .styles import FODT_NS, automatic_styles_xml, styles_xml from .styles import FODT_NS, automatic_styles_xml, styles_xml
@dataclass @dataclass
class EmitState: class EmitState(BlockVisitor):
body: list[str] = field(default_factory=list) body: list[str] = field(default_factory=list)
pending_caption: tuple[str | None, str | None, bool] | None = None pending_caption: tuple[str | None, str | None, bool] | None = None
biblio: list[tuple[str, str]] = field(default_factory=list) biblio: list[tuple[str, str]] = field(default_factory=list)
@@ -31,55 +41,75 @@ class EmitState:
def w(self, s: str = "") -> None: def w(self, s: str = "") -> None:
self.body.append(s) self.body.append(s)
def on_heading(self, element) -> None:
_emit_heading(self, element)
def on_table(self, element, caption_id: str | None, caption_text: str | None) -> None:
_emit_table(self, element, caption_id, caption_text)
def on_code(self, element, lang: str) -> None:
_emit_code(self, element, lang)
def on_list(self, element) -> None:
_emit_list(self, element)
def on_equation(self, element) -> None:
_emit_equation(self, element)
def on_image(self, image) -> None:
_emit_image(self, image)
def on_body_text(self, children) -> None:
text = _inline(children, self).strip()
if text:
self.w(_p("Standard", text))
def on_quote(self, element) -> None:
for child in element.children:
if type(child).__name__ == "Paragraph":
self.w(_p("Standard", _inline(child.children, self)))
def on_other(self, element) -> None:
self.on_body_text(element.children)
class _FodtInline(InlineRenderer):
def __init__(self, st: EmitState | None):
self.st = st
def escape(self, text: str) -> str:
return escape_text(text)
def code_span(self, node) -> str:
return f'<text:span text:style-name="Tcode">{escape_text(node.children)}</text:span>'
def emphasis(self, inner: str) -> str:
return f'<text:span text:style-name="Titalic">{inner}</text:span>'
def strong_emphasis(self, inner: str) -> str:
return f'<text:span text:style-name="Tbold">{inner}</text:span>'
def link(self, node, inner: str) -> str:
href = escape_text(node.dest)
return f'<text:a xlink:type="simple" xlink:href="{href}">{inner}</text:a>'
def reference(self, node) -> str:
key = f"{node.type.lower()}:{node.name}"
num = (self.st.labels.get(key) if self.st else None) or "?"
return escape_text(f"{reference_full_name(node.type)} {num}")
def inline_equation(self, node) -> str:
return (
f'<text:span text:style-name="Tcode">'
f"{escape_text(node.latex_equation)}</text:span>"
)
def line_break(self) -> str:
return "<text:line-break/>"
def _inline(children, st: EmitState | None = None) -> str: def _inline(children, st: EmitState | None = None) -> str:
parts: list[str] = [] return render_inline(children, _FodtInline(st))
for child in children or []:
t = type(child).__name__
if t in ("RawText", "Literal"):
parts.append(escape_text(getattr(child, "children", "") or ""))
elif t == "CodeSpan":
parts.append(
f'<text:span text:style-name="Tcode">{escape_text(child.children)}</text:span>'
)
elif t == "Emphasis":
parts.append(
f'<text:span text:style-name="Titalic">{_inline(child.children, st)}</text:span>'
)
elif t == "StrongEmphasis":
parts.append(
f'<text:span text:style-name="Tbold">{_inline(child.children, st)}</text:span>'
)
elif t == "Link":
href = escape_text(child.dest)
parts.append(
f'<text:a xlink:type="simple" xlink:href="{href}">'
f"{_inline(child.children, st)}</text:a>"
)
elif t == "Reference":
full = {
"рисунок": "Рисунок",
"таблица": "Таблица",
"листинг": "Листинг",
"формула": "формула",
}.get(child.type.lower(), child.type)
key = f"{child.type.lower()}:{child.name}"
num = (st.labels.get(key) if st else None) or "?"
parts.append(escape_text(f"{full} {num}"))
elif t == "InlineEquation":
parts.append(
f'<text:span text:style-name="Tcode">'
f"{escape_text(child.latex_equation)}</text:span>"
)
elif t == "Image":
parts.append("")
elif t == "LineBreak":
parts.append("<text:line-break/>")
elif hasattr(child, "children"):
parts.append(_inline(child.children, st))
else:
parts.append(escape_text(str(child)))
return "".join(parts)
def _plain_inline(children) -> str: def _plain_inline(children) -> str:
@@ -109,10 +139,9 @@ def _h(level: int, content: str) -> str:
def _emit_heading(st: EmitState, heading) -> None: def _emit_heading(st: EmitState, heading) -> None:
text = _inline(heading.children, st).strip() text = _inline(heading.children, st).strip()
plain = _plain_inline(heading.children).strip() plain = _plain_inline(heading.children).strip()
upper = plain.upper()
level = min(int(heading.level), 3) level = min(int(heading.level), 3)
if upper == "СОДЕРЖАНИЕ" or upper.startswith("СОДЕРЖАНИЕ"): if is_contents_heading(plain):
st.w(_p("Centered", escape_text("СОДЕРЖАНИЕ"))) st.w(_p("Centered", escape_text("СОДЕРЖАНИЕ")))
st.w( st.w(
'<text:table-of-content text:style-name="Standard" text:protected="false" ' '<text:table-of-content text:style-name="Standard" text:protected="false" '
@@ -132,32 +161,6 @@ def _emit_heading(st: EmitState, heading) -> None:
st.w(_h(level, text)) st.w(_h(level, text))
def _emit_paragraph(st: EmitState, para) -> None:
text_probe = ""
for ch in para.children or []:
if type(ch).__name__ in ("RawText", "Literal"):
text_probe += ch.children or ""
m = re.match(r"^\[([^\]]+)\]:\s*(.+)$", text_probe.strip())
if m:
st.biblio.append((m.group(1), m.group(2).strip()))
return
has_image = any(type(c).__name__ == "Image" for c in (para.children or []))
if has_image:
for c in para.children:
if type(c).__name__ == "Image":
_emit_image(st, c)
others = [c for c in para.children if type(c).__name__ != "Image"]
t = _inline(others, st).strip()
if t:
st.w(_p("Standard", t))
return
raw = _inline(para.children, st).strip()
if raw:
st.w(_p("Standard", raw))
def _mime_for(path: Path) -> str: def _mime_for(path: Path) -> str:
ext = path.suffix.lower() ext = path.suffix.lower()
return { return {
@@ -173,13 +176,7 @@ def _mime_for(path: Path) -> str:
def _emit_image(st: EmitState, img) -> None: def _emit_image(st: EmitState, img) -> None:
dest = img.dest dest = img.dest
title = getattr(img, "title", None) or "" caption_id, caption_text = parse_image_caption(img, getattr(img, "unique_name", None))
caption_id = getattr(img, "unique_name", None)
caption_text = title
if title.startswith("%"):
parts = title[1:].split(None, 1)
caption_id = parts[0]
caption_text = parts[1] if len(parts) > 1 else ""
if st.pending_caption and not caption_text: if st.pending_caption and not caption_text:
caption_id = caption_id or st.pending_caption[0] caption_id = caption_id or st.pending_caption[0]
@@ -345,10 +342,7 @@ def _emit_table_row(
def _emit_code(st: EmitState, block, lang: str) -> None: def _emit_code(st: EmitState, block, lang: str) -> None:
source = block.children[0].children if block.children else "" source = block.children[0].children if block.children else ""
cap_id, cap_text, _with_listing = (None, None, False) cap_id, cap_text, _with_listing = st.take_caption()
if st.pending_caption:
cap_id, cap_text, _with_listing = st.pending_caption
st.pending_caption = None
lang_l = (lang or "").strip().lower() lang_l = (lang or "").strip().lower()
if lang_l in DIAGRAM_LANGS: if lang_l in DIAGRAM_LANGS:
@@ -402,10 +396,7 @@ def _emit_list(st: EmitState, lst) -> None:
def _emit_equation(st: EmitState, eq) -> None: def _emit_equation(st: EmitState, eq) -> None:
cap_id = None cap_id = st.take_caption()[0]
if st.pending_caption:
cap_id = st.pending_caption[0]
st.pending_caption = None
latex = eq.latex_equation.strip() latex = eq.latex_equation.strip()
bookmark = "" bookmark = ""
if cap_id: if cap_id:
@@ -413,20 +404,16 @@ def _emit_equation(st: EmitState, eq) -> None:
st.w(_p("Centered", bookmark + f'<text:span text:style-name="Tcode">{escape_text(latex)}</text:span>')) st.w(_p("Centered", bookmark + f'<text:span text:style-name="Tcode">{escape_text(latex)}</text:span>'))
def emit_body(md_text: str, *, work_dir: Path, emdash_to_hyphen: bool = False) -> EmitState: def _assign_numbers(st: EmitState, children) -> None:
st = EmitState(work_dir=work_dir) """
pre = preprocess_markdown(md_text, emdash_to_hyphen=emdash_to_hyphen) Number captioned objects up front so `@Тип:id` references resolve even when
# Two-pass: first collect captions/labels roughly by walking once for numbers, they appear before the object they point at.
# then emit. Simple approach: single pass (refs before definition show "?"). """
# Better: first pass assign numbers, second emit.
doc = markdown.parse(pre)
# Pass 1 — assign numbers for captions attached to tables/images/listings
pending = None pending = None
for el in doc.children: for el in children:
name = type(el).__name__ name = type(el).__name__
if name == "Caption": if name == "Caption":
pending = (el.unique_name, el.text, getattr(el, "with_listing", False)) pending = caption_tuple(el)
continue continue
if name == "Table" and pending: if name == "Table" and pending:
st.tab_n += 1 st.tab_n += 1
@@ -441,66 +428,30 @@ def emit_body(md_text: str, *, work_dir: Path, emdash_to_hyphen: bool = False) -
for c in el.children or []: for c in el.children or []:
if type(c).__name__ == "Image": if type(c).__name__ == "Image":
st.fig_n += 1 st.fig_n += 1
cid = pending[0] cid = parse_image_caption(c, pending[0])[0]
title = getattr(c, "title", "") or ""
if title.startswith("%"):
cid = title[1:].split(None, 1)[0]
st.labels[f"рисунок:{cid}"] = str(st.fig_n) st.labels[f"рисунок:{cid}"] = str(st.fig_n)
pending = None pending = None
elif name == "Equation" and pending: elif name == "Equation" and pending:
pending = None pending = None
def emit_body(md_text: str, *, work_dir: Path, emdash_to_hyphen: bool = False) -> EmitState:
st = EmitState(work_dir=work_dir)
pre = preprocess_markdown(md_text, emdash_to_hyphen=emdash_to_hyphen)
# Two-pass: first collect captions/labels roughly by walking once for numbers,
# then emit. Simple approach: single pass (refs before definition show "?").
# Better: first pass assign numbers, second emit.
doc = markdown.parse(pre)
_assign_numbers(st, doc.children)
# reset counters for emit # reset counters for emit
st.fig_n = 0 st.fig_n = 0
st.tab_n = 0 st.tab_n = 0
st.lst_n = 0 st.lst_n = 0
st.pending_caption = None st.pending_caption = None
for el in doc.children: walk_blocks(doc.children, st)
name = type(el).__name__
if name == "BlankLine":
continue
if name == "Caption":
st.pending_caption = (
el.unique_name,
el.text,
getattr(el, "with_listing", False),
)
continue
if name == "Heading":
_emit_heading(st, el)
continue
if name == "Paragraph":
_emit_paragraph(st, el)
continue
if name == "Table":
cid = ct = None
if st.pending_caption:
cid, ct, _ = st.pending_caption
st.pending_caption = None
_emit_table(st, el, cid, ct)
continue
if name in ("FencedCode", "CodeBlock"):
lang = getattr(el, "lang", "") or ""
_emit_code(st, el, lang)
continue
if name == "List":
_emit_list(st, el)
continue
if name == "Equation":
_emit_equation(st, el)
continue
if name == "TOC":
continue
if name == "Quote":
for ch in el.children:
if type(ch).__name__ == "Paragraph":
st.w(_p("Standard", _inline(ch.children, st)))
continue
if hasattr(el, "children"):
t = _inline(el.children, st).strip()
if t:
st.w(_p("Standard", t))
if st.biblio: if st.biblio:
st.w(_h(1, escape_text("СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ"))) st.w(_h(1, escape_text("СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ")))
+3 -8
View File
@@ -2,9 +2,10 @@
from __future__ import annotations from __future__ import annotations
import re
from xml.sax.saxutils import escape as _xml_escape from xml.sax.saxutils import escape as _xml_escape
from mdexport.labels import sanitize_label
def escape_text(s: str) -> str: def escape_text(s: str) -> str:
if not s: if not s:
@@ -12,11 +13,5 @@ def escape_text(s: str) -> str:
return _xml_escape(s, {"\"": "&quot;", "'": "&apos;"}) return _xml_escape(s, {"\"": "&quot;", "'": "&apos;"})
_LABEL_RE = re.compile(r"[^\w\-]+", re.UNICODE)
def xml_id(name: str | None, prefix: str = "obj") -> str: def xml_id(name: str | None, prefix: str = "obj") -> str:
if not name: return sanitize_label(name, prefix, "-")
return prefix
cleaned = _LABEL_RE.sub("-", name.strip()).strip("-")
return f"{prefix}-{cleaned or 'x'}"
+198
View File
@@ -0,0 +1,198 @@
# -*- mode: python ; coding: utf-8 -*-
"""Faster onefile freeze: fonts + plantuml.jar + optional Graphviz + onefile.
Keeps matplotlib (system font lookup) and bundles plantuml.jar.
Graphviz (vendor/) is included when present — for offline DFD.
Speeds Analysis by pinning the Agg backend, skipping duplicate pygments
collection (the stock hook already pulls all lexers), and not probing Qt/Gtk.
"""
import os
import sys
from pathlib import Path
from PyInstaller.utils.hooks import (
collect_data_files,
collect_dynamic_libs,
collect_submodules,
)
# Isolated matplotlib hook subprocesses inherit this and skip GUI backend probes.
os.environ["MPLBACKEND"] = "Agg"
ROOT = Path(SPECPATH).resolve()
sys.path.insert(0, str(ROOT / "scripts"))
from pyi_bundle import collect_tkinterdnd2, prepare_tcltk_env, sanitize_analysis # noqa: E402
prepare_tcltk_env()
datas = [
(str(ROOT / "md2gost" / "Template.docx"), "md2gost"),
(str(ROOT / "md2gost" / "TitleTemplate.docx"), "md2gost"),
(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():
raise SystemExit(
"md2gost.fast.spec: нет md2gost/vendor/plantuml.jar — "
"сначала scripts/fetch_plantuml.py (jar обязателен в portable exe)"
)
datas.append((str(_plantuml), "md2gost/vendor"))
_graphviz = ROOT / "md2gost" / "vendor" / "graphviz"
if (_graphviz / "bin" / "dot.exe").is_file() or (_graphviz / "bin" / "dot").is_file():
from md2gost.dfd import strip_graphviz_tcl_libs
strip_graphviz_tcl_libs(_graphviz)
datas.append((str(_graphviz), "md2gost/vendor/graphviz"))
binaries = []
hiddenimports = collect_submodules("md2gost")
hiddenimports += collect_submodules("word2md")
try:
hiddenimports += collect_submodules("data_flow_diagram")
except Exception:
pass
datas += collect_data_files("certifi")
datas += collect_data_files("lxml")
datas += collect_data_files("latex2mathml")
datas += collect_data_files("docx")
# Explicit mpl-data so DejaVu + matplotlib font cache files are always in the exe.
# hook-matplotlib.py also collects this; PyInstaller dedupes by dest path.
datas += collect_data_files("matplotlib", includes=["mpl-data/**"])
for _pkg in ("mermaidx", "termaid", "quickjs", "quickjs_ng", "resvg_py", "playwright"):
try:
datas += collect_data_files(_pkg)
except Exception:
pass
for pkg in ("lxml", "freetype", "quickjs", "resvg_py"):
try:
binaries += collect_dynamic_libs(pkg)
except Exception:
pass
hiddenimports += [
"lxml",
"lxml.etree",
"lxml._elementpath",
"PIL",
"PIL.Image",
"PIL.PngImagePlugin",
"pygments",
"pygments.lexers",
"pygments.formatters",
"freetype",
"docx",
"docxcompose",
"docxcompose.composer",
"marko",
"marko.ext.gfm",
"latex2mathml",
"latex2mathml.converter",
"requests",
"certifi",
"matplotlib",
"matplotlib.font_manager",
"matplotlib.ft2font",
"tkinter",
"tkinter.filedialog",
"tkinter.messagebox",
"tkinter.ttk",
"mermaidx",
"mermaidx.diagram",
"mermaidx.engines",
"mermaidx.engines.quickjs_engine",
"termaid",
"quickjs",
"resvg_py",
"playwright",
"playwright.sync_api",
]
# Do NOT collect_submodules("pygments.lexers") here — hook-pygments.py already
# collects all lexers/formatters/styles once during Analysis.
_dnd_datas, _dnd_hidden = collect_tkinterdnd2()
datas += _dnd_datas
hiddenimports += _dnd_hidden
excludes = [
"pytest",
"PyQt5",
"PyQt6",
"PySide2",
"PySide6",
"IPython",
"jupyter",
"notebook",
"zmq",
"scipy",
"pandas",
"tornado",
"numpy.tests",
"matplotlib.tests",
"matplotlib.testing",
"matplotlib.backends.qt_compat",
"matplotlib.backends.backend_qtagg",
"matplotlib.backends.backend_qtcairo",
"matplotlib.backends.backend_qt5agg",
"matplotlib.backends.backend_qt5cairo",
"matplotlib.backends.backend_qt5",
"matplotlib.backends.backend_qt",
"matplotlib.backends.backend_gtk3",
"matplotlib.backends.backend_gtk3agg",
"matplotlib.backends.backend_gtk3cairo",
"matplotlib.backends.backend_gtk4",
"matplotlib.backends.backend_gtk4agg",
"matplotlib.backends.backend_gtk4cairo",
"matplotlib.backends.backend_macosx",
"matplotlib.backends.backend_tkagg",
"matplotlib.backends.backend_tkcairo",
"matplotlib.backends.backend_wx",
"matplotlib.backends.backend_wxagg",
"matplotlib.backends.backend_wxcairo",
"matplotlib.backends.backend_nbagg",
"matplotlib.backends.backend_webagg",
"PIL.ImageQt",
]
a = Analysis(
[str(ROOT / "scripts" / "md2gost_exe.py")],
pathex=[str(ROOT)],
binaries=binaries,
datas=datas,
hiddenimports=hiddenimports,
hookspath=[],
hooksconfig={
"matplotlib": {"backends": "Agg"},
},
runtime_hooks=[
str(ROOT / "scripts" / "pyi_rth_tcltk.py"),
str(ROOT / "scripts" / "pyi_rth_mplbackend.py"),
],
excludes=excludes,
noarchive=False,
)
sanitize_analysis(a)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name="md2gost",
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=False,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
+155
View File
@@ -0,0 +1,155 @@
# -*- mode: python ; coding: utf-8 -*-
import sys
from pathlib import Path
from PyInstaller.utils.hooks import (
collect_data_files,
collect_dynamic_libs,
collect_submodules,
)
ROOT = Path(SPECPATH).resolve()
sys.path.insert(0, str(ROOT / "scripts"))
from pyi_bundle import collect_tkinterdnd2, prepare_tcltk_env, sanitize_analysis # noqa: E402
prepare_tcltk_env()
datas = [
(str(ROOT / "md2gost" / "Template.docx"), "md2gost"),
(str(ROOT / "md2gost" / "TitleTemplate.docx"), "md2gost"),
(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():
datas.append((str(_plantuml), "md2gost/vendor"))
_graphviz = ROOT / "md2gost" / "vendor" / "graphviz"
if (_graphviz / "bin" / "dot.exe").is_file() or (_graphviz / "bin" / "dot").is_file():
from md2gost.dfd import strip_graphviz_tcl_libs
strip_graphviz_tcl_libs(_graphviz)
datas.append((str(_graphviz), "md2gost/vendor/graphviz"))
binaries = []
hiddenimports = collect_submodules("md2gost")
hiddenimports += collect_submodules("word2md")
try:
hiddenimports += collect_submodules("data_flow_diagram")
except Exception:
pass
datas += collect_data_files("certifi")
datas += collect_data_files("lxml")
datas += collect_data_files("latex2mathml")
datas += collect_data_files("docx")
datas += collect_data_files("matplotlib", includes=["mpl-data/**"])
for _pkg in ("mermaidx", "termaid", "quickjs", "quickjs_ng", "resvg_py", "playwright"):
try:
datas += collect_data_files(_pkg)
except Exception:
pass
for pkg in ("lxml", "freetype", "quickjs", "resvg_py"):
try:
binaries += collect_dynamic_libs(pkg)
except Exception:
pass
hiddenimports += [
"lxml",
"lxml.etree",
"lxml._elementpath",
"PIL",
"PIL.Image",
"PIL.PngImagePlugin",
"pygments",
"pygments.lexers",
"pygments.formatters",
"freetype",
"docx",
"docxcompose",
"docxcompose.composer",
"marko",
"marko.ext.gfm",
"latex2mathml",
"latex2mathml.converter",
"requests",
"certifi",
"matplotlib",
"matplotlib.font_manager",
"matplotlib.ft2font",
"tkinter",
"tkinter.filedialog",
"tkinter.messagebox",
"tkinter.ttk",
"mermaidx",
"mermaidx.diagram",
"mermaidx.engines",
"mermaidx.engines.quickjs_engine",
"termaid",
"quickjs",
"resvg_py",
"playwright",
"playwright.sync_api",
]
hiddenimports += collect_submodules("pygments.lexers")
hiddenimports += collect_submodules("marko")
_dnd_datas, _dnd_hidden = collect_tkinterdnd2()
datas += _dnd_datas
hiddenimports += _dnd_hidden
excludes = [
"pytest",
"PyQt5",
"PyQt6",
"PySide2",
"PySide6",
"IPython",
"jupyter",
"notebook",
"zmq",
"matplotlib.backends.qt_compat",
"matplotlib.backends.backend_qtagg",
"matplotlib.backends.backend_qt5agg",
"matplotlib.backends.backend_qt5",
"matplotlib.backends.backend_qt",
"matplotlib.backends.backend_gtk3",
"matplotlib.backends.backend_gtk4",
"matplotlib.backends.backend_macosx",
]
a = Analysis(
[str(ROOT / "scripts" / "md2gost_exe.py")],
pathex=[str(ROOT)],
binaries=binaries,
datas=datas,
hiddenimports=hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=[str(ROOT / "scripts" / "pyi_rth_tcltk.py")],
excludes=excludes,
noarchive=False,
)
sanitize_analysis(a)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name="md2gost",
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=False,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
+14
View File
@@ -0,0 +1,14 @@
{
"student": "",
"group": "",
"metadata": {
"author_source": "os",
"author": "",
"last_modified_by": "",
"title": "",
"subject": "",
"keywords": "",
"comments": "Создано при помощи md2gost (ТЗ МИРЭА)",
"category": ""
}
}
+126 -15
View File
@@ -2,14 +2,42 @@
Конвертер Markdown → DOCX по методическим указаниям РТУ МИРЭА (ГОСТ 7.32-2017) на базе [md2gost](https://github.com/benzlokzik/md2gost). Конвертер Markdown → DOCX по методическим указаниям РТУ МИРЭА (ГОСТ 7.32-2017) на базе [md2gost](https://github.com/benzlokzik/md2gost).
**Полная документация:** [`docs/`](../docs/) (быстрый старт, синтаксис, схемы, типы, CLI/GUI, промпты).
## Установка ## Установка
```bash ```bash
poetry install uv sync # окружение по uv.lock (канонический lock-файл)
# или # или
pip install -e . pip install -e .
``` ```
## GUI
Без файла или с `--gui` открывается окно: перетащите `.md`, выберите параметры, нажмите «Конвертировать». CLI при этом тот же (`python -m md2gost файл.md …`).
```bash
python -m md2gost
python -m md2gost --gui
python -m md2gost --gui report.md --type PIS_custom
md2gost-gui
```
На Windows файл можно бросить из Проводника в верхнюю область окна. Клик по области — выбор через диалог. Все флаги CLI есть в форме (тип, нумерация, TOC, тире, `---` → разрыв страницы, титул/задание, диаграммы, проверка ТЗ).
Вкладки **Инструкция** и **Промпт для ИИ** — справка и копирование системного промпта (с опциональным дописыванием UML-схем) в буфер.
### Сборка exe (Windows)
Двойной клик по `build-exe.bat` в корне репозитория (нужен Python 3.10+ в PATH). Результат: `dist\md2gost.exe`.
```bat
build-exe.bat
build-exe.bat nopause
```
Двойной клик по exe — GUI. CLI: `md2gost.exe report.md -o report.docx --type coursework`.
## CLI ## CLI
```bash ```bash
@@ -31,7 +59,7 @@ python -m md2gost report.md -o report.docx --type PIS_custom --check
# Титул: «Отчёт по практическим работам …» — отдельный DOCX: # Титул: «Отчёт по практическим работам …» — отдельный DOCX:
python -m md2gost report.md -o report.docx --type PIS_custom --title title.docx python -m md2gost report.md -o report.docx --type PIS_custom --title title.docx
# Курсовая АПИД (источники 7–20, проверка пунктов 2.1–2.4): # Курсовая АПИД (источники 7–20, проверка пунктов 2.1–2.4):
python -m md2gost report.md -o report.docx --type APID_coursework --check --no-emdash-to-hyphen --title title.docx --assignment assignment.docx python -m md2gost report.md -o report.docx --type APID_coursework --check --title title.docx --assignment assignment.docx
``` ```
Пример: [`examples/pis_custom.md`](../examples/pis_custom.md). Пример: [`examples/pis_custom.md`](../examples/pis_custom.md).
@@ -57,8 +85,12 @@ python -m md2gost report.md -o report.docx --toc manual
### Тире (`--emdash-to-hyphen` / `--no-emdash-to-hyphen`) ### Тире (`--emdash-to-hyphen` / `--no-emdash-to-hyphen`)
По умолчанию типографское «—» заменяется на «-» (в тексте и подписях). По умолчанию типографское «—» **сохраняется** (как в методичке: тире с пробелами, дефис в диапазонах).
Оставить длинное тире по ГОСТ: `--no-emdash-to-hyphen`. Заменить «—» на «-»: `--emdash-to-hyphen`.
### Разрыв страницы (`---` / `--hr-pagebreak`)
Строка `---` (также `***`, `___`) на отдельной строке по умолчанию **игнорируется**. Разрыв страницы: `--hr-pagebreak` или галочка в GUI.
## Синтаксис (кратко) ## Синтаксис (кратко)
@@ -70,9 +102,10 @@ python -m md2gost report.md -o report.docx --toc manual
| Таблица | `%id Подпись` перед таблицей + `@Таблица:id` | | Таблица | `%id Подпись` перед таблицей + `@Таблица:id` |
| Склеивание ячеек | `^` — rowspan (ячейка сверху), `>` — colspan (ячейка слева) | | Склеивание ячеек | `^` — rowspan (ячейка сверху), `>` — colspan (ячейка слева) |
| Листинг | `%id Подпись` перед code fence | | Листинг | `%id Подпись` перед code fence |
| Диаграмма UML/BPMN/C4 | `%id Подпись` + ````uml` / ````bpmn` / ````c4` → PNG (Рисунок); `+listing` — ещё и Листинг | | Диаграмма UML / Mermaid / IDEF0 / DFD / схемы | `%id Подпись` + ````uml` / ````uml-c4` / ````mermaid` / ````idef0` / ````dfd` → Рисунок; `+listing` — ещё и Листинг. Схемы в `md2gost.schemes.json` |
| Формула | `%eq1` + `$$…$$` + `@Формула:eq1` (номер только при ссылке) | | Формула | `%eq1` + `$$…$$` + `@Формула:eq1` (номер только при ссылке) |
| Источник | `[1]` в тексте; `[1]: …` в списке | | Источник | `[1]` в тексте (кликабельная ссылка на пункт списка); `[1]: …` в списке |
| Разрыв страницы | `---` на отдельной строке + `--hr-pagebreak` (по умолчанию `---` игнорируется) |
### Таблицы со склеиванием ### Таблицы со склеиванием
@@ -98,22 +131,44 @@ python -m md2gost report.md -o report.docx --toc manual
Разные шапки «первый раз / продолжение» есть в LaTeX (`longtable`), не в Office. Разные шапки «первый раз / продолжение» есть в LaTeX (`longtable`), не в Office.
Наша оценка высоты строк ≠ вёрстка Word → если резать таблицу в скрипте, получается Наша оценка высоты строк ≠ вёрстка Word → если резать таблицу в скрипте, получается
mid-page «Продолжение…» (как было на 2.4). Поэтому по умолчанию таблицу **не режем**. mid-page «Продолжение…» (как было на 2.4). По умолчанию режим **`word`**: после save
Word COM режет по реальной пагинации. Без Word — укажите `off` или поставьте Word + pywin32.
| Режим | Поведение | | Режим | Поведение |
|--------|-----------| |--------|-----------|
| **`off`** / **`soft`** (по умолчанию) | Одна таблица Word; перенос строк делает Word. Без автоподписи. Первая строка — повторяющаяся шапка (`tblHeader`). «Продолжение…» — вручную в markdown, если нужно | | **`word`** (по умолчанию) | Как `off` при рендере; после save Word COM: `Split` + «Продолжение Таблицы N». Нужны Windows, Word, pywin32. Шапка на продолжении не повторяется (вкл: `--table-repeat-header`). Таблицы с вертикальным merge (`^`): Word не умеет `Split` при vMerge — merge временно снимается, после разреза восстанавливается в обоих фрагментах |
| **`off`** / **`soft`** | Одна таблица Word; перенос строк делает Word. Без автоподписи. Первая строка — повторяющаяся шапка (`tblHeader`). «Продолжение…» — вручную в markdown, если нужно |
| **`legacy`** | Режем по нашей оценке высоты + «Продолжение…» с `page_break_before` (могут быть дыры) | | **`legacy`** | Режем по нашей оценке высоты + «Продолжение…» с `page_break_before` (могут быть дыры) |
| **`caption`** | Режем по оценке + явный PageBreak + «Продолжение…» (то же ограничение точности) | | **`caption`** | Режем по оценке + явный PageBreak + «Продолжение…» (то же ограничение точности) |
```bash ```bash
python -m md2gost report.md -o report.docx --table-continuation word
python -m md2gost report.md -o report.docx --table-continuation off python -m md2gost report.md -o report.docx --table-continuation off
python -m md2gost report.md -o report.docx --table-continuation caption python -m md2gost report.md -o report.docx --table-continuation caption
``` ```
### Продолжение листинга (`--listing-continuation`)
Те же режимы, что у таблиц. По умолчанию **`word`**.
| Режим | Поведение |
|--------|-----------|
| **`word`** (по умолчанию) | После save Word COM + «Продолжение Листинга N» (Windows + Word + pywin32) |
| **`off`** / **`soft`** | Один блок кода; пагинацию делает Word. «Продолжение…» — вручную в markdown, если нужно |
| **`legacy`** | Режем по оценке высоты + «Продолжение Листинга N» с `page_break_before` |
| **`caption`** | Режем по оценке + явный PageBreak + «Продолжение Листинга N» |
```bash
python -m md2gost report.md -o report.docx --listing-continuation word
python -m md2gost report.md -o report.docx --listing-continuation off
python -m md2gost report.md -o report.docx --listing-continuation caption
```
### Диаграммы ### Диаграммы
```markdown В отчёте пишите так (пример в 4 обратных кавычках, чтобы вложенный ` ```uml ` не ломал разметку):
````markdown
%usecase1 Диаграмма прецедентов +listing %usecase1 Диаграмма прецедентов +listing
```uml ```uml
@@ -122,19 +177,73 @@ actor Student
Student --> (Login) Student --> (Login)
@enduml @enduml
``` ```
````
Широкая схема на альбомной странице — флаг `+landscape` в той же строке `%`:
%arch1 Архитектура +landscape
```uml-c4
Person(user, "Студент")
System(app, "Портал")
``` ```
Рендер (по приоритету): ````markdown
%arch1 Архитектура +landscape
1. `PLANTUML_JAR` / `--plantuml-jar` + Java → `plantuml.jar` ```uml-c4
2. `KROKI_URL` / `--kroki-url` (по умолчанию `http://localhost:8000`) Person(user, "Студент")
3. remote `https://kroki.io` при `--diagram-fallback remote` (по умолчанию; предупреждение в лог) System(app, "Портал")
```
````
Кэш PNG: `{каталог_md}/.md2gost-cache/`. Схемы (`c4`, `usecase`, свои): при первом запуске рядом с приложением создаётся `md2gost.schemes.json`. В markdown — оградка `uml-<id>` или короткое `<id>`:
````markdown
%arch C4
```uml-c4
Person(user, "Студент")
System(app, "Портал")
Rel(user, app, "логин")
```
````
URL в `includes` схемы скачиваются в кэш (`md2gost.include-cache.json` + папка `include-cache/`). CLI: `--schemes path.json`. **Mermaid** — ````mermaid` / ````mmd`: системный браузер → Playwright Chromium → QuickJS (`mermaidx`) → Kroki (свой `--kroki-url` или kroki.io). **IDEF0** — ````idef0`: локальный Pillow (рамка NIST, стрелки ICOM). **DFD** — ````dfd`: [data-flow-diagram](https://github.com/pbauermeister/dfd) → вшитый/системный Graphviz или Kroki (`scripts/fetch_graphviz.py`, GUI «Скачать Graphviz»).
Обычному пользователю jar/Kroki указывать не нужно. Порядок для UML:
1. Вшитый / скачанный `plantuml.jar` + Java (exe кладёт jar внутрь; иначе качаем в `%LOCALAPPDATA%\md2gost\`)
2. Локальный Kroki (`KROKI_URL` / `--kroki-url`, по умолчанию `http://localhost:8000`)
3. Интернет `https://kroki.io` при `--diagram-fallback remote` (по умолчанию)
Mermaid:
1. Системный Chrome / Edge (Playwright channel, ничего не тащим)
2. Playwright Chromium в `%LOCALAPPDATA%\md2gost\ms-playwright` — GUI «Скачать headless Chromium» или `md2gost --install-chromium`
3. QuickJS + mermaid.js (`mermaidx`) — офлайн без браузера
4. Локальный Kroki → `https://kroki.io` при `--diagram-fallback remote`
DFD (````dfd`):
1. Пакет `data-flow-diagram` (зависимость md2gost) → DOT
2. Graphviz: вшитый `md2gost/vendor/graphviz` (сборка exe / `scripts/fetch_graphviz.py`) → `%LOCALAPPDATA%\md2gost\graphviz` (GUI «Скачать Graphviz») → `PATH`
3. Локальный Kroki → `https://kroki.io` при `--diagram-fallback remote`
Формат в Word: `--diagram-format png` (по умолчанию; PlantUML и локальный Mermaid с `--diagram-scale`, по умолчанию 2 — только качество, размер на странице как при 1) или `svg` — вектор через `svgBlip` + PNG-запасной (Word 2016+; LibreOffice покажет растр).
Широкие схемы/таблицы: в подписи флаг `+landscape` — отдельная альбомная A4-страница, затем снова книжная.
Свой jar — только если нужен другой файл: `--plantuml-jar` или поле на вкладке «Диаграммы».
Кэш: `{каталог_md}/.md2gost-cache/` (`*.png`, при svg ещё `*.svg`).
```bash ```bash
python -m md2gost report.md -o report.docx --plantuml-jar C:\tools\plantuml.jar python scripts/fetch_plantuml.py
python scripts/fetch_graphviz.py
python scripts/fetch_mermaid.py
python -m md2gost --install-chromium
python -m md2gost report.md -o report.docx --diagram-fallback local python -m md2gost report.md -o report.docx --diagram-fallback local
python -m md2gost report.md -o report.docx --diagram-format svg
``` ```
Подробности и ИИ-промпт: [`prompts/`](../prompts/). Подробности и ИИ-промпт: [`prompts/`](../prompts/).
@@ -144,3 +253,5 @@ PDF через LaTeX (XeLaTeX, шаблон МИРЭА): [`md2latex/README.md`](
## Проверки ## Проверки
`--check` печатает замечания по структуре, «рис.», ссылкам во введении, числу/возрасту источников, приложениям и т.д. `--strict` завершает процесс с кодом 1 при ошибках. `--check` печатает замечания по структуре, «рис.», ссылкам во введении, числу/возрасту источников, приложениям и т.д. `--strict` завершает процесс с кодом 1 при ошибках.
`--check-pages` — пост-проверка полупустых страниц **через Microsoft Word** (Windows + Word + `pip install pywin32`). Все находки помечены как эвристика и **могут быть ложными**; не влияют на `--strict`. Можно вызвать для готового файла: `python -m md2gost report.docx --check-pages`. Макрос Word: [`scripts/check_page_fill.bas`](../scripts/check_page_fill.bas).
Binary file not shown.
+9
View File
@@ -1,8 +1,17 @@
"""md2gost — Markdown → DOCX (MIREA TZ / GOST).""" """md2gost — Markdown → DOCX (MIREA TZ / GOST)."""
import os import os
import sys
def package_dir() -> str: def package_dir() -> str:
"""Directory of the md2gost package (Template.docx, mml2omml, etc.).""" """Directory of the md2gost package (Template.docx, mml2omml, etc.)."""
if getattr(sys, "frozen", False):
meipass = getattr(sys, "_MEIPASS", None)
if meipass:
bundled = os.path.join(meipass, "md2gost")
if os.path.isfile(os.path.join(bundled, "Template.docx")):
return bundled
if os.path.isfile(os.path.join(meipass, "Template.docx")):
return meipass
return os.path.dirname(os.path.abspath(__file__)) return os.path.dirname(os.path.abspath(__file__))
+287 -109
View File
@@ -1,41 +1,46 @@
#!/usr/bin/env python #!/usr/bin/env python
from argparse import ArgumentParser, BooleanOptionalAction from argparse import ArgumentParser, BooleanOptionalAction
import os
import os.path
import sys import sys
from getpass import getuser
from docx import Document from .doc_metadata import AUTHOR_SOURCES, DEFAULT_DOC_COMMENTS
from .pipeline import ConvertRequest, convert, should_launch_gui
from .converter import Converter
from .profiles import ( from .profiles import (
DEFAULT_HEADING_NUMBERING,
DEFAULT_TABLE_CONTINUATION,
DEFAULT_LISTING_CONTINUATION,
DEFAULT_TOC_MODE,
DOC_TYPES, DOC_TYPES,
HEADING_NUMBERING_MODES, HEADING_NUMBERING_MODES,
DEFAULT_HEADING_NUMBERING,
TOC_MODES,
DEFAULT_TOC_MODE,
TABLE_CONTINUATION_MODES, TABLE_CONTINUATION_MODES,
DEFAULT_TABLE_CONTINUATION, LISTING_CONTINUATION_MODES,
get_profile, TOC_MODES,
) )
from .checker import check_markdown, format_report
def main(): def build_parser() -> ArgumentParser:
parser = ArgumentParser( parser = ArgumentParser(
prog="md2gost", prog="md2gost",
description=( description=(
"Генерация DOCX-отчётов из Markdown по ТЗ МИРЭА / ГОСТ. " "Генерация DOCX-отчётов из Markdown по ТЗ МИРЭА / ГОСТ. "
"Типы: coursework/practice/vkr, APID_coursework, PIS_custom. " "Типы: coursework/practice/vkr, APID_coursework, PIS_custom. "
"Без файла или с --gui открывается окно. "
"DOCX → Markdown: python -m word2md файл.docx (или md2gost файл.docx). "
"FODT: python -m md2fodt …" "FODT: python -m md2fodt …"
), ),
) )
parser.add_argument("filename", help="Путь до исходного markdown файла") parser.add_argument(
"filename", nargs="?",
help="Путь до .md (сборка DOCX), .docx (импорт в Markdown) или .docx с --check-pages",
)
parser.add_argument(
"--gui", action="store_true",
help="Открыть графический интерфейс (можно сразу передать .md)",
)
parser.add_argument("-o", "--output", help="Путь до сгенерированного .docx") parser.add_argument("-o", "--output", help="Путь до сгенерированного .docx")
parser.add_argument("-t", "--template", help="Путь до шаблона .docx") parser.add_argument("-t", "--template", help="Путь до шаблона .docx")
parser.add_argument( parser.add_argument(
"--type", dest="doc_type", choices=DOC_TYPES, default="coursework", "--type", dest="doc_type", choices=DOC_TYPES, default="practice",
help="Тип: coursework | practice | vkr | PIS_custom | APID_coursework", help="Тип: practice | coursework | vkr | PIS_custom | APID_coursework (по умолчанию practice)",
) )
parser.add_argument( parser.add_argument(
"--heading-numbering", "--heading-numbering",
@@ -64,27 +69,137 @@ def main():
default=DEFAULT_TABLE_CONTINUATION, default=DEFAULT_TABLE_CONTINUATION,
help=( help=(
"Таблицы длиннее страницы: " "Таблицы длиннее страницы: "
"off/soft — одна таблица, пагинация Word, без авто«Продолжение» (по умолчанию); " "word — после сохранения Word COM режет по реальной пагинации + «Продолжение…» "
"(по умолчанию; нужны Windows, Word, pywin32); "
"off/soft — одна таблица, пагинация Word, без авто«Продолжение»; "
"legacy/caption — режем по оценке высоты и вставляем «Продолжение…» " "legacy/caption — режем по оценке высоты и вставляем «Продолжение…» "
"(оценка ≠ Word, возможны артефакты). " "(оценка ≠ Word, возможны артефакты). "
f"По умолчанию: {DEFAULT_TABLE_CONTINUATION}." f"По умолчанию: {DEFAULT_TABLE_CONTINUATION}."
), ),
) )
parser.add_argument( parser.add_argument(
"--emdash-to-hyphen", "--listing-continuation",
action=BooleanOptionalAction, choices=LISTING_CONTINUATION_MODES,
default=True, default=DEFAULT_LISTING_CONTINUATION,
help=( help=(
"Автозамена типографского тире «—» на дефис «-» в тексте и подписях. " "Листинги длиннее страницы: "
"Включено по умолчанию; отключить: --no-emdash-to-hyphen." "word — после сохранения Word COM + «Продолжение Листинга…» "
"(по умолчанию; нужны Windows, Word, pywin32); "
"off/soft — один блок, пагинация Word, без авто«Продолжение»; "
"legacy/caption — режем по оценке высоты и вставляем «Продолжение Листинга…» "
"(оценка ≠ Word, возможны артефакты). "
f"По умолчанию: {DEFAULT_LISTING_CONTINUATION}."
),
)
parser.add_argument(
"--table-repeat-header",
action=BooleanOptionalAction,
default=False,
help=(
"При --table-continuation word: повторять шапку таблицы на каждом фрагменте "
"после разрыва. По умолчанию выкл."
),
)
parser.add_argument(
"--emdash-to-hyphen",
action=BooleanOptionalAction,
default=False,
help=(
"Автозамена типографского тире «—» на дефис «-» в тексте и подписях. "
"По умолчанию выключено (тире «—» по методичке); включить: --emdash-to-hyphen."
),
)
parser.add_argument(
"--hr-pagebreak",
action=BooleanOptionalAction,
default=False,
help=(
"Строка «---» / «***» / «___» — разрыв страницы Word. "
"По умолчанию такие строки игнорируются; включить: --hr-pagebreak."
),
)
parser.add_argument("--title", help="DOCX титульного листа (вставляется перед телом; "
"отключает автогенерацию)")
parser.add_argument(
"--auto-title",
action=BooleanOptionalAction,
default=False,
help=(
"Сгенерировать титул из info_conv.yaml / блока ```title (нужны ФИО и группа). "
"По умолчанию выкл.; включить: --auto-title. Явный --title отключает генератор."
), ),
) )
parser.add_argument("--title", help="DOCX титульного листа (вставляется перед телом)")
parser.add_argument("--assignment", help="DOCX бланка задания") parser.add_argument("--assignment", help="DOCX бланка задания")
parser.add_argument(
"--student",
help="ФИО студента для автотитула (иначе md2gost.user.json)",
)
parser.add_argument(
"--group",
help="Учебная группа для автотитула (иначе md2gost.user.json)",
)
parser.add_argument(
"--doc-author-from",
choices=AUTHOR_SOURCES,
default=None,
help=(
"Автор в свойствах DOCX: os — имя пользователя ОС; "
"student — ФИО из профиля; custom — --doc-author / md2gost.user.json. "
"Иначе берётся из Настройки → Метаданные (по умолчанию os)."
),
)
parser.add_argument(
"--doc-author",
default=None,
help="Автор в свойствах DOCX (перебивает --doc-author-from)",
)
parser.add_argument(
"--doc-last-modified-by",
default=None,
help="Поле «Кем изменён» в свойствах DOCX; пусто — как автор",
)
parser.add_argument(
"--doc-title",
default=None,
help="Название в свойствах DOCX (File → Info)",
)
parser.add_argument(
"--doc-subject",
default=None,
help="Тема в свойствах DOCX",
)
parser.add_argument(
"--doc-keywords",
default=None,
help="Ключевые слова (теги) в свойствах DOCX",
)
parser.add_argument(
"--doc-comments",
default=None,
help=(
"Примечание в свойствах DOCX. "
f"По умолчанию: «{DEFAULT_DOC_COMMENTS}». "
"Пустая строка — не писать примечание."
),
)
parser.add_argument(
"--doc-category",
default=None,
help="Категория в свойствах DOCX",
)
parser.add_argument("--check", help="Проверить markdown по ТЗ и вывести отчёт", parser.add_argument("--check", help="Проверить markdown по ТЗ и вывести отчёт",
action="store_true") action="store_true")
parser.add_argument("--check-only", help="Только проверка, без генерации документа", parser.add_argument("--check-only", help="Только проверка, без генерации документа",
action="store_true") action="store_true")
parser.add_argument(
"--check-pages",
help=(
"После конвертации (или для готового .docx) проверить полупустые страницы "
"через Microsoft Word. Эвристика — возможны ложные срабатывания. "
"Нужны Windows, Word и pywin32."
),
action="store_true",
)
parser.add_argument("--strict", help="Код выхода 1 при ошибках проверки", parser.add_argument("--strict", help="Код выхода 1 при ошибках проверки",
action="store_true") action="store_true")
parser.add_argument("--syntax-highlighting", help="Подсветка синтаксиса в листингах", parser.add_argument("--syntax-highlighting", help="Подсветка синтаксиса в листингах",
@@ -101,110 +216,173 @@ def main():
"--diagram-fallback", "--diagram-fallback",
choices=["local", "remote", "off"], choices=["local", "remote", "off"],
default="remote", default="remote",
help="Если локальный рендер UML недоступен: remote (kroki.io), local (ошибка), off", help="Если локальный рендер недоступен: remote (kroki.io), local (ошибка), off",
)
parser.add_argument(
"--diagram-format",
choices=["png", "svg"],
default="png",
help="Формат схем в Word: png (по умолчанию) или svg (вектор + PNG-запасной, Word 2016+)",
)
parser.add_argument(
"--diagram-scale",
type=float,
default=2.0,
help=(
"Масштаб растра PlantUML / локального Mermaid (качество); "
"размер на странице как при 1. По умолчанию 2"
),
)
parser.add_argument(
"--page-start",
type=int,
default=None,
metavar="N",
help=(
"Смещение / начальный номер страницы в колонтитуле тела документа. "
"Пусто — сквозная нумерация Word; например 1 — тело начинается с 1"
),
)
parser.add_argument(
"--install-chromium",
action="store_true",
help=(
"Скачать headless Chromium для локального Mermaid "
"в %%LOCALAPPDATA%%\\md2gost\\ms-playwright (и выйти)"
),
)
parser.add_argument(
"--schemes",
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="Добавляет отладочные данные в документ", parser.add_argument("--debug", help="Добавляет отладочные данные в документ",
action="store_true") action="store_true")
return parser
args = parser.parse_args()
filename, output, template, debug = \
args.filename, args.output, args.template, args.debug
if args.syntax_highlighting:
os.environ["SYNTAX_HIGHLIGHTING"] = "1"
from .diagram_renderer import configure_diagrams def request_from_args(args) -> ConvertRequest:
configure_diagrams( return ConvertRequest(
plantuml_jar=args.plantuml_jar, filename=args.filename or "",
kroki_url=args.kroki_url, output=args.output,
fallback=args.diagram_fallback, template=args.template,
)
if not filename.endswith(".md"):
print("Error: filename must have md format")
exit(1)
os.environ["WORKING_DIR"] = os.path.dirname(os.path.abspath(filename)) or "."
with open(filename, encoding="utf-8") as f:
md_text = f.read()
if args.check or args.check_only:
issues = check_markdown(md_text, args.doc_type)
print(format_report(issues))
errors = [i for i in issues if i.severity == "error"]
if args.strict and errors:
sys.exit(1)
if args.check_only:
sys.exit(0 if not errors else (1 if args.strict else 0))
if not output:
output = os.path.basename(filename).replace(".md", ".docx")
elif not output.endswith(".docx"):
print("Error: output file must have docx format")
exit(1)
if not template:
from . import package_dir
template = os.path.join(package_dir(), "Template.docx")
converter = Converter(
filename, output, template, debug,
doc_type=args.doc_type, doc_type=args.doc_type,
heading_numbering=args.heading_numbering, heading_numbering=args.heading_numbering,
emdash_to_hyphen=args.emdash_to_hyphen,
toc_mode=args.toc, toc_mode=args.toc,
table_continuation=args.table_continuation, table_continuation=args.table_continuation,
listing_continuation=args.listing_continuation,
table_repeat_header=bool(getattr(args, "table_repeat_header", False)),
emdash_to_hyphen=args.emdash_to_hyphen,
hr_pagebreak=args.hr_pagebreak,
title=args.title,
assignment=args.assignment,
student=getattr(args, "student", None),
group=getattr(args, "group", None),
doc_author_source=getattr(args, "doc_author_from", None),
doc_author=getattr(args, "doc_author", None),
doc_last_modified_by=getattr(args, "doc_last_modified_by", None),
doc_title=getattr(args, "doc_title", None),
doc_subject=getattr(args, "doc_subject", None),
doc_keywords=getattr(args, "doc_keywords", None),
doc_comments=getattr(args, "doc_comments", None),
doc_category=getattr(args, "doc_category", None),
auto_title=bool(getattr(args, "auto_title", False)),
check=args.check,
check_only=args.check_only,
check_pages=bool(getattr(args, "check_pages", False)),
strict=args.strict,
syntax_highlighting=bool(args.syntax_highlighting),
plantuml_jar=args.plantuml_jar,
kroki_url=args.kroki_url,
diagram_fallback=args.diagram_fallback,
diagram_format=args.diagram_format,
diagram_scale=float(args.diagram_scale),
schemes_path=args.schemes_path,
styles_path=getattr(args, "styles_path", None),
page_number_start=(
int(args.page_start)
if getattr(args, "page_start", None) is not None and int(args.page_start) >= 1
else None
),
debug=args.debug,
open_when_done=bool(args.debug),
) )
converter.convert()
document = converter.document
# Front matter is appended *into* a shell that already has coursework styles. def _argv_needs_console(argv: list[str]) -> bool:
# Never use title.docx as compose base: python-docx default template has """Frozen windowed exe: attach a console for CLI / --help, not for GUI."""
# Calibri + accent-blue headings and would override ГОСТ стили. args = argv[1:]
if args.title or args.assignment: if not args:
return False
if "-h" in args or "--help" in args:
return True
if "--install-chromium" in args:
return True
if "--gui" in args:
return False
return any(not a.startswith("-") for a in args)
def _enable_windows_console() -> None:
if sys.platform != "win32" or not getattr(sys, "frozen", False):
return
try:
import ctypes
kernel32 = ctypes.windll.kernel32
if not kernel32.AttachConsole(0xFFFFFFFF):
if not kernel32.GetConsoleWindow():
kernel32.AllocConsole()
sys.stdout = open("CONOUT$", "w", encoding="utf-8", errors="replace")
sys.stderr = open("CONOUT$", "w", encoding="utf-8", errors="replace")
try: try:
from docxcompose.composer import Composer sys.stdin = open("CONIN$", "r", encoding="utf-8", errors="replace")
except ImportError: except OSError:
print("Error: docxcompose required for --title/--assignment") pass
sys.exit(3) except Exception:
from .styles import apply_document_styles pass
shell = Document(template)
apply_document_styles(shell, get_profile(args.doc_type).style_preset)
body = shell.element.body
for child in list(body):
if not child.tag.endswith("}sectPr"):
body.remove(child)
composer = Composer(shell) def main():
if args.title: if _argv_needs_console(sys.argv):
composer.append(Document(args.title)) _enable_windows_console()
shell.add_page_break() parser = build_parser()
if args.assignment: args = parser.parse_args()
composer.append(Document(args.assignment))
shell.add_page_break()
composer.append(document)
document = composer.doc
apply_document_styles(document, get_profile(args.doc_type).style_preset)
document.core_properties.author = getuser() if getattr(args, "install_chromium", False):
document.core_properties.comments = \ from .mermaid_renderer import (
"Создано при помощи md2gost (ТЗ МИРЭА)" install_playwright_chromium,
playwright_available,
playwright_browsers_dir,
)
document.save(output) if not playwright_available():
print(f"Generated document: {os.path.abspath(output)}") print("playwright не установлен. pip install playwright", file=sys.stderr)
sys.exit(1)
print(f"Устанавливаю Chromium → {playwright_browsers_dir()}")
ok = install_playwright_chromium()
if ok:
print("Готово.")
sys.exit(0)
print("Не удалось установить Chromium.", file=sys.stderr)
sys.exit(1)
if debug: req = request_from_args(args)
import platform
if platform.system() == 'Darwin': if should_launch_gui(args.filename, args.gui):
import subprocess from .gui import run_gui
subprocess.call(('open', output)) run_gui(req)
elif platform.system() == 'Windows': return
os.startfile(output)
else: result = convert(req)
import subprocess sys.exit(result.exit_code)
subprocess.call(('xdg-open', output))
if __name__ == "__main__": if __name__ == "__main__":
+17 -2
View File
@@ -39,6 +39,21 @@ def entries_from_text(text: str) -> list[BiblioEntry]:
return entries return entries
# After citation linking, `[5]: …` often becomes `[]: …` in paragraph.text
# (the digits live in a hyperlink). Still a biblio source line to drop.
BIBLIO_RESIDUE_RE = re.compile(r"^\[\d*(?:\.\d+)?\]:\s*\S")
def is_biblio_source_paragraph(text: str) -> bool:
"""True if paragraph is a [n]: / []: bibliography source line (or empty)."""
stripped = (text or "").strip()
if not stripped:
return True
if entries_from_text(stripped):
return True
return bool(BIBLIO_RESIDUE_RE.match(stripped))
def extract_bibliography_from_markdown(md: str) -> list[BiblioEntry] | list[tuple[str, list[BiblioEntry]]]: def extract_bibliography_from_markdown(md: str) -> list[BiblioEntry] | list[tuple[str, list[BiblioEntry]]]:
""" """
Parse bibliography from raw markdown (reliable even if Marko merges lines). Parse bibliography from raw markdown (reliable even if Marko merges lines).
@@ -126,8 +141,8 @@ def fold_bibliography(renderables: list[Renderable], parent,
consumed += 1 consumed += 1
continue continue
if isinstance(item, Paragraph): if isinstance(item, Paragraph):
text = (item._docx_paragraph.text or "").strip() text = item._docx_paragraph.text or ""
if entries_from_text(text) or (not text): if is_biblio_source_paragraph(text):
consumed += 1 consumed += 1
continue continue
break break
+5
View File
@@ -16,6 +16,11 @@ CITE_RE = re.compile(
) )
def biblio_bookmark_name(key: str) -> str:
"""Word bookmark name for bibliography key (dots → underscores)."""
return "biblio_" + str(key).replace(".", "_")
@dataclass @dataclass
class BiblioEntry: class BiblioEntry:
key: str key: str
+4 -1
View File
@@ -41,13 +41,16 @@ class Bibliography(Renderable):
@staticmethod @staticmethod
def _make_entry(parent: Parented, e: BiblioEntry) -> Paragraph: def _make_entry(parent: Parented, e: BiblioEntry) -> Paragraph:
from .bibliography import biblio_bookmark_name
p = Paragraph(parent) p = Paragraph(parent)
try: try:
p.style = "Bibliography" p.style = "Bibliography"
except KeyError: except KeyError:
pass pass
# Avoid hyphen-splitting URLs: put whole entry in one run via direct text # Avoid hyphen-splitting URLs: put whole entry in one run via direct text
run = p._docx_paragraph.add_run(f"{e.key}. {e.text}") p._docx_paragraph.add_run(f"{e.key}. {e.text}")
p.wrap_with_bookmark(biblio_bookmark_name(e.key))
return p return p
def render(self, previous_rendered: RenderedInfo, layout_state: LayoutState) -> Generator[ def render(self, previous_rendered: RenderedInfo, layout_state: LayoutState) -> Generator[
+133 -14
View File
@@ -26,6 +26,7 @@ SPECIAL_UNNUMBERED = {
} }
HEADING_RE = re.compile(r"^(#{1,6})\s+(\*?)(.+)$", re.M) HEADING_RE = re.compile(r"^(#{1,6})\s+(\*?)(.+)$", re.M)
EMPTY_HEADING_RE = re.compile(r"^(#{1,6})[^\n\S]*\*?[^\n\S]*$", re.M)
FOOTNOTE_RE = re.compile(r"\[\^[^\]]+\]") FOOTNOTE_RE = re.compile(r"\[\^[^\]]+\]")
RIS_RE = re.compile(r"(?i)\bрис\.") RIS_RE = re.compile(r"(?i)\bрис\.")
TABL_RE = re.compile(r"(?i)\bтабл\.") TABL_RE = re.compile(r"(?i)\bтабл\.")
@@ -199,6 +200,19 @@ def check_special_headings(text: str) -> list[Issue]:
return issues return issues
def check_empty_headings(text: str) -> list[Issue]:
"""Заголовок без текста: пронумерованный, но безымянный раздел в содержании."""
issues = []
for m in EMPTY_HEADING_RE.finditer(text):
issues.append(Issue(
"heading.empty", "warning",
"Заголовок без текста: разделы, подразделы и пункты должны иметь "
"заголовки, чётко и кратко отражающие их содержание",
_line_of(text, m.start()),
))
return issues
def check_forbidden_abbreviations(text: str) -> list[Issue]: def check_forbidden_abbreviations(text: str) -> list[Issue]:
issues = [] issues = []
for m in RIS_RE.finditer(text): for m in RIS_RE.finditer(text):
@@ -258,12 +272,36 @@ def check_bibliography(text: str, profile: DocProfile) -> list[Issue]:
n = len(entries) n = len(entries)
if profile.sectional_biblio: if profile.sectional_biblio:
# Count per ## section roughly # Count per ## section inside bibliography
if n < profile.min_sources: sections: list[tuple[str, int]] = []
current_title: str | None = None
current_count = 0
for line in block.splitlines():
hm = re.match(r"^#{2,6}\s+(\*?)(.+)$", line)
if hm:
if current_title is not None:
sections.append((current_title, current_count))
current_title = hm.group(2).strip()
current_count = 0
continue
if BIBLIO_LINE_RE.match(line.strip()):
current_count += 1
if current_title is not None:
sections.append((current_title, current_count))
if sections:
for title, cnt in sections:
if cnt < profile.min_sources:
issues.append(Issue(
"biblio.count", "warning",
f"ВКР: в разделе списка «{title}» желательно ≥{profile.min_sources} "
f"источников (сейчас {cnt})",
))
elif n < profile.min_sources:
issues.append(Issue( issues.append(Issue(
"biblio.count", "warning", "biblio.count", "warning",
f"ВКР: в каждом разделе списка желательно ≥{profile.min_sources} источников " f"ВКР: список должен делиться на разделы; в каждом ≥{profile.min_sources} "
f"(сейчас всего {n})", f"источников (сейчас всего {n}, разделов нет)",
)) ))
else: else:
if n < profile.min_sources: if n < profile.min_sources:
@@ -288,13 +326,10 @@ def check_bibliography(text: str, profile: DocProfile) -> list[Issue]:
)) ))
# Citation order vs first appearance # Citation order vs first appearance
# Exclude intro/conclusion from citation scan for order
body_for_cites = text body_for_cites = text
cites = find_citations(body_for_cites) cites = find_citations(body_for_cites)
# Filter cites that appear only in intro/conclusion — still listed
keys = [e[0] for e in entries] keys = [e[0] for e in entries]
if cites and keys: if cites and keys:
# First N unique cites should match order of keys for simple lists
if not profile.sectional_biblio: if not profile.sectional_biblio:
expected = cites[:len(keys)] expected = cites[:len(keys)]
if keys != expected and set(keys) == set(expected): if keys != expected and set(keys) == set(expected):
@@ -313,16 +348,15 @@ def check_bibliography(text: str, profile: DocProfile) -> list[Issue]:
def check_object_refs(text: str) -> list[Issue]: def check_object_refs(text: str) -> list[Issue]:
issues = [] issues = []
# Captions / images
labels = set(re.findall(r"^%(\w+)", text, re.M)) labels = set(re.findall(r"^%(\w+)", text, re.M))
# image titles with %id
labels.update(re.findall(r'!\[[^\]]*\]\([^)]*%(\w+)', text)) labels.update(re.findall(r'!\[[^\]]*\]\([^)]*%(\w+)', text))
refs = set(re.findall(r"@[\wА-Яа-я]+:(\w+)", text)) refs = set(re.findall(r"@[\wА-Яа-я]+:(\w+)", text))
for lab in labels: for lab in labels:
# At least one @?:lab or word reference — soft check if lab not in refs:
if lab not in refs and f"@{lab}" not in text: issues.append(Issue(
# only warn if label looks intentional "ref.unused", "warning",
pass f"Метка «{lab}» объявлена, но в тексте нет ссылки @…:{lab}",
))
for ref in refs: for ref in refs:
if ref not in labels: if ref not in labels:
issues.append(Issue( issues.append(Issue(
@@ -342,6 +376,75 @@ def check_appendices(text: str) -> list[Issue]:
f"Буква «{letter}» не используется для обозначения приложений", f"Буква «{letter}» не используется для обозначения приложений",
_line_of(text, m.start()), _line_of(text, m.start()),
)) ))
# Multiple lettered appendices should have a list after # *ПРИЛОЖЕНИЯ
apps = list(re.finditer(
r"^#{1,6}\s+\*?Приложение\s+[А-ЯA-ZЁ]\b",
text, re.M | re.I,
))
# Filter out the section header «ПРИЛОЖЕНИЯ» / bare «ПРИЛОЖЕНИЕ»
lettered = []
for m in apps:
line = m.group(0)
if re.search(r"(?i)приложение\s+[А-ЯA-ZЁ]", line):
lettered.append(m)
if len(lettered) >= 2:
sec = re.search(
r"^#\s*\*?\s*ПРИЛОЖЕНИЯ?\s*$",
text, re.M | re.I,
)
if sec:
between = text[sec.end(): lettered[0].start()]
# Expect a plain-text list (not only blank / headings)
has_list = bool(re.search(r"(?im)^\s*[-–—*]|\bприложение\s+[А-ЯA-ZЁ]\b", between))
has_prose = bool(re.search(r"[А-Яа-яA-Za-z]{3,}", between))
if not (has_list or has_prose):
issues.append(Issue(
"appendix.toc", "warning",
"При нескольких приложениях после «ПРИЛОЖЕНИЯ» нужен перечень "
"(основной текст) с номерами и названиями",
_line_of(text, sec.start()),
))
return issues
def check_continuation_hints(
text: str,
*,
table_continuation: str = "off",
listing_continuation: str = "off",
syntax_highlighting: bool = False,
strict: bool = False,
) -> list[Issue]:
"""Warn that Word cannot auto-insert «Продолжение…» when mode is off/soft."""
issues: list[Issue] = []
soft = {"off", "soft"}
if table_continuation in soft and _TABLE_BLOCK_RE.search(text):
issues.append(Issue(
"table.continuation", "warning",
"Word сам не вставит «Продолжение Таблицы N» при переносе. "
"Варианты: --table-continuation word (точный разрыв через Word COM) "
"или caption (оценка высоты ≠ вёрстка Word, возможны артефакты).",
))
# Long fenced code blocks (likely listings)
long_listing = False
for m in re.finditer(r"^```[^\n]*\n([\s\S]*?)^```", text, re.M):
if m.group(1).count("\n") >= 40:
long_listing = True
break
if listing_continuation in soft and long_listing:
issues.append(Issue(
"listing.continuation", "warning",
"Длинный листинг: Word сам не вставит «Продолжение Листинга N». "
"Варианты: --listing-continuation word или caption.",
))
if syntax_highlighting:
issues.append(Issue(
"listing.syntax_highlighting",
"error" if strict else "warning",
"Подсветка синтаксиса задаёт полужирный и цвет в содержимом листинга. "
"Табл. 4.1 методички требует «Начертание Обычный», «Цвет текста Авто (чёрный)».",
))
return issues return issues
@@ -404,11 +507,20 @@ def check_table_merge(text: str) -> list[Issue]:
return issues return issues
def check_markdown(text: str, doc_type: str = "coursework") -> list[Issue]: def check_markdown(
text: str,
doc_type: str = "coursework",
*,
table_continuation: str = "off",
listing_continuation: str = "off",
syntax_highlighting: bool = False,
strict: bool = False,
) -> list[Issue]:
profile = get_profile(doc_type) profile = get_profile(doc_type)
issues: list[Issue] = [] issues: list[Issue] = []
issues.extend(check_structure(text, profile)) issues.extend(check_structure(text, profile))
issues.extend(check_special_headings(text)) issues.extend(check_special_headings(text))
issues.extend(check_empty_headings(text))
issues.extend(check_pis_headings(text, profile)) issues.extend(check_pis_headings(text, profile))
issues.extend(check_forbidden_abbreviations(text)) issues.extend(check_forbidden_abbreviations(text))
if profile.require_intro_conclusion: if profile.require_intro_conclusion:
@@ -417,6 +529,13 @@ def check_markdown(text: str, doc_type: str = "coursework") -> list[Issue]:
issues.extend(check_object_refs(text)) issues.extend(check_object_refs(text))
issues.extend(check_appendices(text)) issues.extend(check_appendices(text))
issues.extend(check_table_merge(text)) issues.extend(check_table_merge(text))
issues.extend(check_continuation_hints(
text,
table_continuation=table_continuation,
listing_continuation=listing_continuation,
syntax_highlighting=syntax_highlighting,
strict=strict,
))
return issues return issues
+43 -8
View File
@@ -1,3 +1,5 @@
import os
import docx import docx
from docx.document import Document from docx.document import Document
@@ -15,7 +17,9 @@ from .profiles import (
DEFAULT_TOC_MODE, DEFAULT_TOC_MODE,
TOC_MODES, TOC_MODES,
DEFAULT_TABLE_CONTINUATION, DEFAULT_TABLE_CONTINUATION,
DEFAULT_LISTING_CONTINUATION,
TABLE_CONTINUATION_MODES, TABLE_CONTINUATION_MODES,
LISTING_CONTINUATION_MODES,
) )
from .label_pass import ( from .label_pass import (
assign_numbers, assign_numbers,
@@ -26,18 +30,24 @@ from .label_pass import (
from .renderable.heading import Heading from .renderable.heading import Heading
from .renderable.toc import ToC from .renderable.toc import ToC
from .renderable.table import Table from .renderable.table import Table
from .renderable.listing import Listing
from .renderable.diagram import DiagramFigure
class Converter: class Converter:
"""Converts markdown file to docx file (MIREA TZ).""" """Converts markdown file to docx file (MIREA TZ)."""
def __init__(self, input_path: str, output_path: str, def __init__(self, input_path: str,
template_path: str = None, debug: bool = False, template_path: str = None, debug: bool = False,
doc_type: str = "coursework", doc_type: str = "practice",
heading_numbering: str = DEFAULT_HEADING_NUMBERING, heading_numbering: str = DEFAULT_HEADING_NUMBERING,
emdash_to_hyphen: bool = False, emdash_to_hyphen: bool = False,
toc_mode: str = DEFAULT_TOC_MODE, toc_mode: str = DEFAULT_TOC_MODE,
table_continuation: str = DEFAULT_TABLE_CONTINUATION): table_continuation: str = DEFAULT_TABLE_CONTINUATION,
listing_continuation: str = DEFAULT_LISTING_CONTINUATION,
hr_pagebreak: bool = False,
styles_path: str | None = None,
markdown_text: str | None = None):
if heading_numbering not in HEADING_NUMBERING_MODES: if heading_numbering not in HEADING_NUMBERING_MODES:
raise ValueError( raise ValueError(
f"heading_numbering must be one of {HEADING_NUMBERING_MODES}, " f"heading_numbering must be one of {HEADING_NUMBERING_MODES}, "
@@ -52,22 +62,39 @@ class Converter:
f"table_continuation must be one of {TABLE_CONTINUATION_MODES}, " f"table_continuation must be one of {TABLE_CONTINUATION_MODES}, "
f"got {table_continuation!r}" f"got {table_continuation!r}"
) )
self._output_path = output_path if listing_continuation not in LISTING_CONTINUATION_MODES:
raise ValueError(
f"listing_continuation must be one of {LISTING_CONTINUATION_MODES}, "
f"got {listing_continuation!r}"
)
self._doc_type = doc_type self._doc_type = doc_type
self._heading_numbering = heading_numbering self._heading_numbering = heading_numbering
self._emdash_to_hyphen = emdash_to_hyphen self._emdash_to_hyphen = emdash_to_hyphen
self._toc_mode = toc_mode self._toc_mode = toc_mode
self._table_continuation = table_continuation 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._profile = get_profile(doc_type)
self._document: Document = docx.Document(template_path) self._document: Document = docx.Document(template_path)
self._document._body.clear_content() 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 "."
os.environ["WORKING_DIR"] = md_dir
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 self._debugger = Debugger(self._document) if debug else None
with open(input_path, encoding="utf-8") as f: if markdown_text is not None:
raw = f.read() raw = markdown_text
else:
with open(input_path, encoding="utf-8") as f:
raw = f.read()
self._raw_markdown = raw self._raw_markdown = raw
self._preprocessed = preprocess_markdown(raw, emdash_to_hyphen=emdash_to_hyphen) self._preprocessed = preprocess_markdown(raw, emdash_to_hyphen=emdash_to_hyphen)
self.parser = Parser(self._document, self._preprocessed) self.parser = Parser(self._document, self._preprocessed, hr_pagebreak=hr_pagebreak)
def convert(self): def convert(self):
renderables = list(self.parser.parse()) renderables = list(self.parser.parse())
@@ -85,6 +112,10 @@ class Converter:
r.set_toc_mode(self._toc_mode) r.set_toc_mode(self._toc_mode)
elif isinstance(r, Table): elif isinstance(r, Table):
r.set_continuation_mode(self._table_continuation) r.set_continuation_mode(self._table_continuation)
elif isinstance(r, Listing):
r.set_continuation_mode(self._listing_continuation)
elif isinstance(r, DiagramFigure) and r.listing:
r.listing.set_continuation_mode(self._listing_continuation)
formula_refs = find_formula_refs(self._raw_markdown) formula_refs = find_formula_refs(self._raw_markdown)
registry = assign_numbers( registry = assign_numbers(
@@ -106,6 +137,10 @@ class Converter:
def document(self) -> Document: def document(self) -> Document:
return self._document return self._document
@property
def style_config(self):
return self._style_config
@property @property
def raw_markdown(self) -> str: def raw_markdown(self) -> str:
return self._raw_markdown return self._raw_markdown
+519
View File
@@ -0,0 +1,519 @@
"""DFD via pbauermeister/data-flow-diagram (Graphviz).
Fence: ```dfd (aliases ```uml-dfd, ```data-flow-diagram, ```yourdon, …).
DSL: https://github.com/pbauermeister/dfd
Pipeline:
1. Compile DSL → Graphviz DOT (`data-flow-diagram` Python API).
2. Rasterize: bundled/system Graphviz (`dot`/`neato`), else Kroki `graphviz`.
"""
from __future__ import annotations
import logging
import os
import re
import shutil
import subprocess
import sys
import tempfile
import zipfile
from pathlib import Path
import requests
from . import package_dir
_log = logging.getLogger(__name__)
CACHE_KEY_PREFIX = "dfd-v3-pbauermeister\n"
# Context diagrams use neato; everything else uses dot (matches upstream templates).
_ENGINE_CONTEXT = "neato"
_ENGINE_DEFAULT = "dot"
# Official Windows portable build (same pattern as plantuml.jar vendor).
GRAPHVIZ_VERSION = "16.1.0"
GRAPHVIZ_ZIP_URL = (
"https://gitlab.com/api/v4/projects/4207231/packages/generic/graphviz-releases/"
f"{GRAPHVIZ_VERSION}/windows_10_cmake_Release_Graphviz-{GRAPHVIZ_VERSION}-win64.zip"
)
# Portable Graphviz ships Tcl 8.6.10 as tcl86t.dll. CPython 3.11's tkinter needs
# exactly 8.6.12 — if both land in the onefile extract, GUI dies on Tk().
# `dot` does not need the Tcl bindings.
GRAPHVIZ_TCL_DLL_NAMES = frozenset(
{
"tcl86t.dll",
"tk86t.dll",
"tcl86.dll",
"tk86.dll",
"tcldot.dll",
"tcldot_builtin.dll",
"tclplan.dll",
"gdtclft.dll",
}
)
class DfdParseError(ValueError):
"""Invalid DFD DSL or render tooling failure surfaced as parse/render error."""
SAMPLE_CONTEXT = """\
style context
entity Client Клиент
entity Warehouse Склад
process System Система учёта заявок
Client --> System заявка
System --> Client статус
System --> Warehouse накладная
"""
SAMPLE_LEVEL0 = """\
entity Client Клиент
entity Warehouse Склад
process Accept Принять заявку
process Register Зарегистрировать
process Ship Сформировать накладную
store Orders Заявки
Client --> Accept заявка
Accept --> Orders новая заявка
Orders --> Register данные заявки
Register --> Ship подтверждённая заявка
Ship --> Warehouse накладная
Ship --> Client уведомление
"""
def vendor_graphviz_root() -> Path:
return Path(package_dir()) / "vendor" / "graphviz"
def cached_graphviz_root() -> Path:
if os.name == "nt":
root = Path(os.environ.get("LOCALAPPDATA") or Path.home()) / "md2gost"
else:
root = Path.home() / ".md2gost"
return root / "graphviz"
def _graphviz_bin_candidates() -> list[Path]:
roots: list[Path] = [vendor_graphviz_root()]
if getattr(sys, "frozen", False):
mei = getattr(sys, "_MEIPASS", None)
if mei:
roots.append(Path(mei) / "md2gost" / "vendor" / "graphviz")
roots.append(Path(mei) / "vendor" / "graphviz")
roots.append(Path(sys.executable).resolve().parent / "graphviz")
roots.append(cached_graphviz_root())
bins: list[Path] = []
for root in roots:
bins.append(root / "bin")
bins.append(root)
return bins
def _engine_in_bin(bin_dir: Path, context: bool) -> Path | None:
if not bin_dir.is_dir():
return None
names = (
(_ENGINE_CONTEXT, _ENGINE_DEFAULT)
if context
else (_ENGINE_DEFAULT, _ENGINE_CONTEXT)
)
for name in names:
for cand in (bin_dir / f"{name}.exe", bin_dir / name):
if cand.is_file():
return cand
return None
def resolve_graphviz_engine(context: bool = False) -> str | None:
"""Path to Graphviz layout engine: vendor → cache → PATH."""
for bin_dir in _graphviz_bin_candidates():
hit = _engine_in_bin(bin_dir, context)
if hit is not None:
return str(hit)
if context:
return _which_many(
_ENGINE_CONTEXT, f"{_ENGINE_CONTEXT}.exe", _ENGINE_DEFAULT, "dot.exe",
)
return _which_many(_ENGINE_DEFAULT, "dot.exe", _ENGINE_CONTEXT, "neato.exe")
def _which_many(*names: str) -> str | None:
for name in names:
hit = shutil.which(name)
if hit:
return hit
return None
def _normalize_extracted_graphviz(extract_dir: Path, dest_root: Path) -> bool:
"""Find bin/dot(.exe) under extract_dir and copy tree into dest_root."""
dot = None
for path in extract_dir.rglob("dot.exe"):
if path.is_file():
dot = path
break
if dot is None:
for path in extract_dir.rglob("dot"):
if path.is_file() and os.access(path, os.X_OK):
dot = path
break
if dot is None:
return False
src_bin = dot.parent
src_root = src_bin.parent if src_bin.name.lower() == "bin" else src_bin
if dest_root.exists():
shutil.rmtree(dest_root)
dest_root.parent.mkdir(parents=True, exist_ok=True)
shutil.copytree(src_root, dest_root)
strip_graphviz_tcl_libs(dest_root)
return (
_engine_in_bin(dest_root / "bin", False) is not None
or _engine_in_bin(dest_root, False) is not None
)
def strip_graphviz_tcl_libs(root: Path) -> int:
"""Remove Graphviz Tcl/Tk DLLs so they cannot shadow CPython's tkinter."""
root = Path(root)
if not root.is_dir():
return 0
removed = 0
bins = [root / "bin", root]
seen: set[Path] = set()
for bin_dir in bins:
if not bin_dir.is_dir():
continue
resolved = bin_dir.resolve()
if resolved in seen:
continue
seen.add(resolved)
for name in GRAPHVIZ_TCL_DLL_NAMES:
path = bin_dir / name
if path.is_file():
path.unlink()
removed += 1
tcl_mod = root / "lib" / "graphviz" / "tcl"
if tcl_mod.is_dir():
shutil.rmtree(tcl_mod)
removed += 1
return removed
def fetch_graphviz(dest_root: Path | None = None, url: str = GRAPHVIZ_ZIP_URL) -> bool:
"""Download official Graphviz win64 ZIP into dest_root (default: vendor)."""
dest = Path(dest_root) if dest_root is not None else vendor_graphviz_root()
if _engine_in_bin(dest / "bin", False) or _engine_in_bin(dest, False):
strip_graphviz_tcl_libs(dest)
return True
try:
_log.info("Downloading Graphviz %s …", GRAPHVIZ_VERSION)
with requests.get(url, stream=True, timeout=180) as resp:
resp.raise_for_status()
with tempfile.TemporaryDirectory(prefix="md2gost-gv-") as tmp:
zpath = Path(tmp) / "graphviz.zip"
with zpath.open("wb") as fh:
for chunk in resp.iter_content(chunk_size=1024 * 256):
if chunk:
fh.write(chunk)
extract_dir = Path(tmp) / "out"
extract_dir.mkdir()
with zipfile.ZipFile(zpath) as zf:
zf.extractall(extract_dir)
if not _normalize_extracted_graphviz(extract_dir, dest):
_log.warning("Graphviz ZIP распакован, но dot.exe не найден")
return False
return True
except Exception as exc:
_log.warning("Не удалось скачать Graphviz: %s", exc)
return False
def resolve_dfd_cli() -> list[str] | None:
"""Argv prefix that compiles DSL → DOT without needing Graphviz for compile."""
py = _which_many("py", "py.exe")
if py:
for ver in ("-3.14", "-3.13", "-3.12", "-3.11"):
try:
probe = subprocess.run(
[py, ver, "-c", "import data_flow_diagram"],
capture_output=True,
timeout=20,
)
if probe.returncode == 0:
return [py, ver, "-c", _API_DOT_RUNNER]
except (OSError, subprocess.TimeoutExpired):
continue
exe = _which_many("data-flow-diagram", "data-flow-diagram.exe")
if exe:
return [exe]
return None
_API_DOT_RUNNER = r"""
import sys
from data_flow_diagram.dfd import build
from data_flow_diagram import model
src_path = sys.argv[1]
text = open(src_path, encoding="utf-8").read()
root = model.SourceLine("", "<md2gost>", None, 0)
opts = model.Options(
format="dot",
background_color=None,
no_graph_title=True,
no_check_dependencies=False,
debug=False,
)
dot, graph_options = build(root, text, "", opts)
sys.stdout.reconfigure(encoding="utf-8") if hasattr(sys.stdout, "reconfigure") else None
sys.stdout.write(dot)
sys.stderr.write("\n__MD2GOST_DFD_CONTEXT__=%s\n" % int(bool(graph_options.is_context)))
"""
def _is_context_diagram(source: str) -> bool:
return bool(re.search(r"(?im)^\s*style\s+context\b", source))
def _compile_dot(source: str) -> tuple[str, bool]:
"""Return (dot_text, is_context). Raises DfdParseError."""
text = (source or "").strip()
if not text:
raise DfdParseError("пустой DFD")
try:
from data_flow_diagram.dfd import build as dfd_build
from data_flow_diagram import model as dfd_model
root = dfd_model.SourceLine("", "<md2gost>", None, 0)
opts = dfd_model.Options(
format="dot",
background_color=None,
no_graph_title=True,
no_check_dependencies=False,
debug=False,
)
dot, graph_options = dfd_build(root, text, "", opts)
return dot, bool(graph_options.is_context)
except ImportError:
pass
except Exception as exc:
raise DfdParseError(str(exc)) from exc
cli = resolve_dfd_cli()
if not cli:
raise DfdParseError(
"Пакет data-flow-diagram не найден. "
"Установите зависимости проекта (Python 3.11+) — "
"https://github.com/pbauermeister/dfd"
)
with tempfile.TemporaryDirectory(prefix="md2gost-dfd-") as tmp:
src_path = Path(tmp) / "diagram.dfd"
src_path.write_text(text + "\n", encoding="utf-8")
if cli[-1] == _API_DOT_RUNNER or (len(cli) >= 2 and cli[-2] == "-c"):
cmd = list(cli) + [str(src_path)]
elif len(cli) == 1:
cmd = cli + [str(src_path), "-f", "dot", "-o", "-", "--no-graph-title"]
else:
cmd = list(cli) + [str(src_path)]
try:
proc = subprocess.run(
cmd,
capture_output=True,
timeout=120,
text=True,
encoding="utf-8",
errors="replace",
)
except FileNotFoundError as exc:
raise DfdParseError(f"Не удалось запустить data-flow-diagram: {exc}") from exc
except subprocess.TimeoutExpired as exc:
raise DfdParseError("Таймаут data-flow-diagram") from exc
if proc.returncode != 0:
err = (proc.stderr or proc.stdout or "").strip() or f"код {proc.returncode}"
raise DfdParseError(f"DFD: {err}")
dot = proc.stdout or ""
if not dot.strip():
raise DfdParseError("data-flow-diagram вернул пустой DOT")
is_ctx = _is_context_diagram(text)
m = re.search(r"__MD2GOST_DFD_CONTEXT__=(\d+)", proc.stderr or "")
if m:
is_ctx = bool(int(m.group(1)))
return dot, is_ctx
def _graphviz_run_env(engine: str) -> dict[str, str]:
env = os.environ.copy()
bin_dir = str(Path(engine).resolve().parent)
env["PATH"] = bin_dir + os.pathsep + env.get("PATH", "")
env["GVBINDIR"] = bin_dir
return env
def _readable_dot(dot: str) -> str:
"""Make Graphviz DFD easier to read in printed Word reports."""
text = dot
text = re.sub(
r'edge\[color=gray fontname="times-italic" fontsize=\d+\]',
'edge[color=black fontname="helvetica" fontsize=13]',
text,
)
text = re.sub(
r'node\[fontname="helvetica" fontsize=\d+\]',
'node[fontname="helvetica" fontsize=13]',
text,
)
if "dpi=" not in text:
text = text.replace("digraph D {", 'digraph D {\n graph[dpi=180 nodesep=0.55 ranksep=0.7]', 1)
return text
def _render_dot_local(dot: str, out_path: Path, fmt: str, *, context: bool) -> bool:
engine = resolve_graphviz_engine(context=context)
if not engine:
return False
try:
subprocess.run(
[engine, f"-T{fmt}", f"-o{out_path}"],
input=_readable_dot(dot),
encoding="utf-8",
check=True,
capture_output=True,
timeout=120,
env=_graphviz_run_env(engine),
)
except (OSError, subprocess.CalledProcessError, subprocess.TimeoutExpired) as exc:
_log.warning("Graphviz %s failed: %s", engine, exc)
return False
return out_path.is_file() and out_path.stat().st_size > 0
def _render_dot_local_combined(
dot: str,
targets: list[tuple[Path, str]],
*,
context: bool,
) -> set[str]:
"""
One Graphviz process for several formats (`-Tpng -o… -Tsvg -o…`).
Returns the formats actually written; the caller retries the rest one by one.
"""
if len(targets) < 2:
return set()
engine = resolve_graphviz_engine(context=context)
if not engine:
return set()
cmd = [engine]
for out_path, fmt in targets:
cmd += [f"-T{fmt}", f"-o{out_path}"]
try:
subprocess.run(
cmd,
input=_readable_dot(dot),
encoding="utf-8",
check=True,
capture_output=True,
timeout=180,
env=_graphviz_run_env(engine),
)
except (OSError, subprocess.CalledProcessError, subprocess.TimeoutExpired) as exc:
_log.warning("Graphviz %s (multi-format) failed: %s", engine, exc)
return set()
return {
fmt for out_path, fmt in targets
if Path(out_path).is_file() and Path(out_path).stat().st_size > 0
}
def _render_dot_kroki(dot: str, out_path: Path, fmt: str) -> bool:
try:
from .diagram_renderer import (
DEFAULT_KROKI_URL,
REMOTE_KROKI_URL,
_CONFIG,
_render_kroki,
)
except Exception:
return False
local = (_CONFIG.kroki_url or os.environ.get("KROKI_URL") or DEFAULT_KROKI_URL).rstrip("/")
if _render_kroki(dot, "graphviz", local, out_path, fmt):
return True
if _CONFIG.fallback == "remote":
return _render_kroki(dot, "graphviz", REMOTE_KROKI_URL, out_path, fmt)
return False
def dfd_engine_status() -> str:
cli_ok = False
try:
import data_flow_diagram # noqa: F401
cli_ok = True
except ImportError:
cli_ok = resolve_dfd_cli() is not None
gv = resolve_graphviz_engine(False)
parts = ["DFD (pbauermeister/data-flow-diagram), оградка ```dfd"]
parts.append(
"компилятор: data-flow-diagram OK"
if cli_ok
else "компилятор: не найден (зависимость md2gost, Python 3.11+)"
)
if gv:
parts.append(f"Graphviz: {gv}")
else:
parts.append(
"Graphviz: нет — «Скачать Graphviz» / scripts/fetch_graphviz.py или Kroki"
)
return "\n".join(parts)
def write_dfd_outputs(
source: str,
out_png: Path,
out_svg: Path | None,
*,
scale: float = 2.0,
) -> bool:
"""Compile DSL and write PNG (+ optional SVG). Raises DfdParseError."""
del scale
dot, is_context = _compile_dot(source)
out_png = Path(out_png)
out_png.parent.mkdir(parents=True, exist_ok=True)
out_svg = Path(out_svg) if out_svg is not None else None
targets: list[tuple[Path, str]] = [(out_png, "png")]
if out_svg is not None:
targets.append((out_svg, "svg"))
done = _render_dot_local_combined(dot, targets, context=is_context)
ok_png = "png" in done or _render_dot_local(dot, out_png, "png", context=is_context)
if not ok_png:
ok_png = _render_dot_kroki(dot, out_png, "png")
if not ok_png:
raise DfdParseError(
"Не удалось отрисовать DFD: скачайте Graphviz "
"(GUI: Диаграммы → Скачать Graphviz, или scripts/fetch_graphviz.py) "
"или включите Kroki (--diagram-fallback remote). "
"DSL: https://github.com/pbauermeister/dfd"
)
if out_svg is not None and "svg" not in done:
ok_svg = _render_dot_local(dot, out_svg, "svg", context=is_context)
if not ok_svg:
_render_dot_kroki(dot, out_svg, "svg")
_log.info("DFD ok → %s", out_png.name)
return True
+261
View File
@@ -0,0 +1,261 @@
"""HTTP(S) include cache: index URL → local file, never overwrite existing cache files."""
from __future__ import annotations
import hashlib
import json
import logging
import re
import sys
from pathlib import Path
from urllib.parse import urlparse, urljoin
import requests
_log = logging.getLogger(__name__)
INCLUDE_CACHE_INDEX = "md2gost.include-cache.json"
INCLUDE_CACHE_DIR = "include-cache"
MAX_INCLUDE_DEPTH = 8
_HTTP_RE = re.compile(r"^https?://", re.I)
_INCLUDE_LINE_RE = re.compile(
r"^[ \t]*!(?:includeurl|include)[ \t]+(?P<q>[\"']?)(?P<ref>[^\s\"']+)(?P=q)",
re.I | re.M,
)
def app_dir() -> Path:
"""Directory next to the running application (exe or cwd for python -m)."""
if getattr(sys, "frozen", False):
return Path(sys.executable).resolve().parent
return Path.cwd()
def include_cache_index_path(base: Path | None = None) -> Path:
return (base or app_dir()) / INCLUDE_CACHE_INDEX
def include_cache_files_dir(base: Path | None = None) -> Path:
return (base or app_dir()) / INCLUDE_CACHE_DIR
def is_http_url(value: str) -> bool:
return bool(_HTTP_RE.match((value or "").strip()))
def _load_index(path: Path) -> dict[str, str]:
if not path.is_file():
return {}
try:
data = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
_log.warning("Не удалось прочитать индекс кэша includes: %s", exc)
return {}
if not isinstance(data, dict):
return {}
out: dict[str, str] = {}
for key, val in data.items():
if isinstance(key, str) and isinstance(val, str) and is_http_url(key):
out[key] = val
return out
def _save_index(path: Path, index: dict[str, str]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
json.dumps(index, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
def _unique_cache_path(url: str, files_dir: Path) -> Path:
"""New file path for URL; never reuse/overwrite an existing cache file."""
files_dir.mkdir(parents=True, exist_ok=True)
parsed = urlparse(url)
base = Path(parsed.path).name or "include.puml"
if not base.lower().endswith((".puml", ".iuml", ".pu", ".txt")):
base = base + ".puml" if "." not in base else base
digest = hashlib.sha256(url.encode("utf-8")).hexdigest()[:10]
stem = Path(base).stem
suffix = Path(base).suffix or ".puml"
# sanitize stem
safe = re.sub(r"[^a-zA-Z0-9._-]+", "_", stem)[:40] or "include"
candidate = files_dir / f"{safe}_{digest}{suffix}"
if not candidate.exists():
return candidate
n = 1
while True:
alt = files_dir / f"{safe}_{digest}_{n}{suffix}"
if not alt.exists():
return alt
n += 1
def _download(url: str, dest: Path) -> None:
dest.parent.mkdir(parents=True, exist_ok=True)
tmp = dest.with_suffix(dest.suffix + ".part")
try:
resp = requests.get(url, timeout=60)
resp.raise_for_status()
tmp.write_bytes(resp.content)
tmp.replace(dest)
except Exception:
tmp.unlink(missing_ok=True)
raise
def resolve_url(
url: str,
*,
base: Path | None = None,
depth: int = 0,
) -> Path:
"""
Return local path for URL. Uses index if present and file exists;
otherwise downloads to a *new* file and records the mapping.
Never overwrites an existing cache file for a different URL.
"""
url = url.strip()
if not is_http_url(url):
raise ValueError(f"Не URL: {url}")
if depth > MAX_INCLUDE_DEPTH:
raise RuntimeError(f"Слишком глубокая цепочка includes ({MAX_INCLUDE_DEPTH}): {url}")
root = base or app_dir()
index_path = include_cache_index_path(root)
files_dir = include_cache_files_dir(root)
index = _load_index(index_path)
cached = index.get(url)
if cached:
path = Path(cached)
if path.is_file():
_rewrite_nested_includes(path, parent_url=url, base=root, depth=depth)
return path
_log.warning("Файл кэша пропал (%s), скачаю заново: %s", path, url)
dest = _unique_cache_path(url, files_dir)
try:
_download(url, dest)
except Exception as exc:
raise RuntimeError(
f"Не удалось скачать include {url}: {exc}. "
"Нет сети и нет кэша — положите файл вручную или проверьте URL."
) from exc
index[url] = str(dest.resolve())
_save_index(index_path, index)
_log.info("Include cached: %s → %s", url, dest)
_rewrite_nested_includes(dest, parent_url=url, base=root, depth=depth)
return dest
def _absolute_include_url(ref: str, parent_url: str | None) -> str | None:
"""Turn include ref into absolute http(s) URL, or None if local/stdlib."""
ref = ref.strip()
if is_http_url(ref):
return ref
if ref.startswith("<") and ref.endswith(">"):
return None # PlantUML stdlib
if parent_url and not Path(ref).is_absolute():
# relative to parent URL directory
return urljoin(parent_url, ref)
return None
def _rewrite_nested_includes(
path: Path, *, parent_url: str, base: Path, depth: int,
) -> None:
"""Rewrite http(s) / relative !include inside a cached .puml to local paths."""
try:
text = path.read_text(encoding="utf-8")
except OSError:
return
changed = False
def repl(match: re.Match[str]) -> str:
nonlocal changed
ref = match.group("ref")
abs_url = _absolute_include_url(ref, parent_url)
if not abs_url:
return match.group(0)
local = resolve_url(abs_url, base=base, depth=depth + 1)
changed = True
local_s = str(local.resolve()).replace("\\", "/")
return f"!include {local_s}"
new_text = _INCLUDE_LINE_RE.sub(repl, text)
if changed and new_text != text:
path.write_text(new_text, encoding="utf-8")
def resolve_include_ref(
ref: str,
*,
base: Path | None = None,
schemes_dir: Path | None = None,
) -> str:
"""
Resolve include entry to a local path string suitable for !include.
Local paths are resolved relative to schemes_dir / diagrams / cwd.
"""
ref = (ref or "").strip()
if not ref:
raise ValueError("Пустой include")
if is_http_url(ref):
return str(resolve_url(ref, base=base).resolve()).replace("\\", "/")
path = Path(ref)
if path.is_file():
return str(path.resolve()).replace("\\", "/")
candidates: list[Path] = []
if schemes_dir:
candidates.append(schemes_dir / ref)
from . import package_dir
candidates.append(Path(package_dir()) / "diagrams" / ref)
candidates.append(Path.cwd() / ref)
for cand in candidates:
if cand.is_file():
return str(cand.resolve()).replace("\\", "/")
raise FileNotFoundError(f"Include не найден: {ref}")
def rewrite_http_includes_in_source(source: str, *, base: Path | None = None) -> str:
"""Replace http(s) !include / !includeurl in prepared source with cached local paths."""
def repl(match: re.Match[str]) -> str:
ref = match.group("ref")
if not is_http_url(ref):
return match.group(0)
local = str(resolve_url(ref, base=base).resolve()).replace("\\", "/")
return f"!include {local}"
return _INCLUDE_LINE_RE.sub(repl, source)
def reset_include_cache(base: Path | None = None) -> int:
"""
Delete index and only files listed in it. Returns number of files removed.
Other files in include-cache/ are left untouched.
"""
root = base or app_dir()
index_path = include_cache_index_path(root)
index = _load_index(index_path)
removed = 0
for _url, path_s in list(index.items()):
path = Path(path_s)
try:
if path.is_file():
path.unlink()
removed += 1
except OSError as exc:
_log.warning("Не удалось удалить %s: %s", path, exc)
if index_path.is_file():
index_path.unlink(missing_ok=True)
return removed
def cache_entry_count(base: Path | None = None) -> int:
return len(_load_index(include_cache_index_path(base or app_dir())))
+693 -79
View File
@@ -1,12 +1,14 @@
"""Render UML/BPMN/C4 fenced blocks to PNG (PlantUML jar / Kroki local / remote).""" """Render UML / Mermaid / scheme fenced blocks to PNG (+ optional SVG)."""
from __future__ import annotations from __future__ import annotations
import hashlib import hashlib
import logging import logging
import os import os
import re
import shutil import shutil
import subprocess import subprocess
import sys
import tempfile import tempfile
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
@@ -15,13 +17,55 @@ from typing import Literal
import requests import requests
from . import package_dir from . import package_dir
from .diagram_schemes import (
configure_schemes,
ensure_user_schemes,
is_diagram_lang,
prepare_with_schemes,
)
DIAGRAM_LANGS = frozenset({"uml", "plantuml", "bpmn", "c4"}) # Back-compat: static set used by older tests; runtime uses is_diagram_lang().
DIAGRAM_LANGS = frozenset(
{
"uml",
"plantuml",
"c4",
"c4context",
"c4component",
"usecase",
"mermaid",
"mmd",
"idef0",
"dfd",
}
)
# Native Pillow renderers — never jar / Kroki.
LOCAL_ONLY_TYPES = frozenset({"idef0", "dfd"})
# Local hi-res path uses a scale-specific cache key.
SCALED_LOCAL_TYPES = frozenset({"mermaid", "idef0", "dfd"})
FallbackMode = Literal["local", "remote", "off"] FallbackMode = Literal["local", "remote", "off"]
DiagramFormat = Literal["png", "svg"]
DEFAULT_KROKI_URL = "http://localhost:8000" DEFAULT_KROKI_URL = "http://localhost:8000"
REMOTE_KROKI_URL = "https://kroki.io" REMOTE_KROKI_URL = "https://kroki.io"
# Sharper raster when Word shrinks the figure to page width (display size stays ×1).
DEFAULT_DIAGRAM_SCALE = 2.0
# Avoid PlantUML clipping large scaled diagrams (default limit is 4096).
PLANTUML_LIMIT_SIZE = "8192"
# Pinned official jar (GPL). Bundled at exe build; otherwise downloaded once to user cache.
PLANTUML_VERSION = "1.2025.4"
PLANTUML_JAR_URL = (
f"https://github.com/plantuml/plantuml/releases/download/"
f"v{PLANTUML_VERSION}/plantuml-{PLANTUML_VERSION}.jar"
)
# Part of the cache key: a new jar draws the same source differently.
PLANTUML_CACHE_PREFIX = f"plantuml-{PLANTUML_VERSION}\n"
# One JVM per chunk; keeps the command line far below the Windows 32k limit.
PLANTUML_BATCH_CHUNK = 60
_log = logging.getLogger(__name__) _log = logging.getLogger(__name__)
@@ -31,6 +75,17 @@ class DiagramConfig:
kroki_url: str | None = None kroki_url: str | None = None
fallback: FallbackMode = "remote" fallback: FallbackMode = "remote"
cache_dir: str | None = None cache_dir: str | None = None
schemes_path: str | None = None
diagram_format: DiagramFormat = "png"
diagram_scale: float = DEFAULT_DIAGRAM_SCALE
@dataclass
class DiagramRenderResult:
png_path: str
svg_path: str | None = None
# PlantUML render scale; Image divides display size by this to keep ×1 layout.
pixel_scale: float = 1.0
_CONFIG = DiagramConfig() _CONFIG = DiagramConfig()
@@ -41,13 +96,34 @@ def configure_diagrams(
kroki_url: str | None = None, kroki_url: str | None = None,
fallback: FallbackMode = "remote", fallback: FallbackMode = "remote",
cache_dir: str | None = None, cache_dir: str | None = None,
schemes_path: str | None = None,
md_dir: str | None = None,
diagram_format: DiagramFormat = "png",
diagram_scale: float | None = None,
) -> None: ) -> None:
global _CONFIG global _CONFIG
ensure_user_schemes()
configure_schemes(schemes_path=schemes_path, md_dir=md_dir)
reset_diagram_batch()
try:
from .mermaid_renderer import ensure_playwright_browsers_env, reset_mermaid_session
ensure_playwright_browsers_env()
reset_mermaid_session()
except Exception:
pass
fmt: DiagramFormat = "svg" if diagram_format == "svg" else "png"
scale = DEFAULT_DIAGRAM_SCALE if diagram_scale is None else float(diagram_scale)
if scale < 1:
scale = 1.0
_CONFIG = DiagramConfig( _CONFIG = DiagramConfig(
plantuml_jar=plantuml_jar or os.environ.get("PLANTUML_JAR"), plantuml_jar=resolve_plantuml_jar(plantuml_jar or None, download=False),
kroki_url=kroki_url or os.environ.get("KROKI_URL"), kroki_url=kroki_url or os.environ.get("KROKI_URL"),
fallback=fallback, fallback=fallback,
cache_dir=cache_dir, cache_dir=cache_dir,
schemes_path=schemes_path,
diagram_format=fmt,
diagram_scale=scale,
) )
@@ -57,85 +133,400 @@ def diagrams_dir() -> Path:
def prepare_source(lang: str, source: str) -> tuple[str, str]: def prepare_source(lang: str, source: str) -> tuple[str, str]:
"""Return (prepared_source, kroki_diagram_type).""" """Return (prepared_source, kroki_diagram_type)."""
lang = (lang or "uml").lower().strip() return prepare_with_schemes(lang, source)
text = source.strip()
if lang == "bpmn":
if "@startbpmn" not in text.lower() and "<definitions" not in text.lower():
# PlantUML BPMN dialect
if not text.startswith("@start"):
text = "@startbpmn\n" + text + "\n@endbpmn"
return text, "bpmn"
if lang == "c4":
if "!include" not in text and "!includeurl" not in text.lower():
# Prefer PlantUML stdlib; also ship local stubs for jar -I path
includes = (
f"!include {diagrams_dir() / 'C4_Container.puml'}\n"
if (diagrams_dir() / "C4_Container.puml").exists()
else "!include <C4/C4_Container>\n"
)
body = text
if body.lower().startswith("@startuml"):
lines = body.splitlines()
text = lines[0] + "\n" + includes + "\n".join(lines[1:])
else:
text = f"@startuml\n{includes}{body}\n@enduml"
elif not text.lower().startswith("@start"):
text = f"@startuml\n{text}\n@enduml"
return text, "plantuml"
# uml / plantuml
if not text.lower().startswith("@start"):
text = f"@startuml\n{text}\n@enduml"
return text, "plantuml"
def _cache_path(source: str, cache_dir: str | None = None) -> Path: def _cache_root(cache_dir: str | None = None) -> Path:
root = cache_dir or _CONFIG.cache_dir root = cache_dir or _CONFIG.cache_dir
if not root: if not root:
wd = os.environ.get("WORKING_DIR", ".") wd = os.environ.get("WORKING_DIR", ".")
root = os.path.join(wd, ".md2gost-cache") root = os.path.join(wd, ".md2gost-cache")
Path(root).mkdir(parents=True, exist_ok=True) path = Path(root)
digest = hashlib.sha256(source.encode("utf-8")).hexdigest()[:24] path.mkdir(parents=True, exist_ok=True)
return Path(root) / f"{digest}.png" return path
def _cache_digest(source: str, *, cache_prefix: str = "") -> str:
payload = (cache_prefix + source) if cache_prefix else source
return hashlib.sha256(payload.encode("utf-8")).hexdigest()[:24]
def _cache_paths(
source: str,
cache_dir: str | None = None,
*,
cache_prefix: str = "",
) -> tuple[Path, Path]:
root = _cache_root(cache_dir)
digest = _cache_digest(source, cache_prefix=cache_prefix)
return root / f"{digest}.png", root / f"{digest}.svg"
def _java_available() -> bool: def _java_available() -> bool:
return shutil.which("java") is not None return shutil.which("java") is not None
def _render_plantuml_jar(source: str, out_png: Path, jar: str) -> bool: def vendor_plantuml_path() -> Path:
if not jar or not os.path.isfile(jar) or not _java_available(): return Path(package_dir()) / "vendor" / "plantuml.jar"
def cached_plantuml_path() -> Path:
if os.name == "nt":
root = Path(os.environ.get("LOCALAPPDATA") or Path.home()) / "md2gost"
else:
root = Path.home() / ".md2gost"
return root / "plantuml.jar"
def iter_plantuml_candidates(explicit: str | None = None) -> list[Path]:
paths: list[Path] = []
if explicit:
paths.append(Path(explicit))
env = os.environ.get("PLANTUML_JAR")
if env:
paths.append(Path(env))
paths.append(vendor_plantuml_path())
if getattr(sys, "frozen", False):
mei = getattr(sys, "_MEIPASS", None)
if mei:
paths.append(Path(mei) / "md2gost" / "vendor" / "plantuml.jar")
paths.append(Path(mei) / "vendor" / "plantuml.jar")
paths.append(Path(sys.executable).resolve().parent / "plantuml.jar")
paths.append(cached_plantuml_path())
seen: set[str] = set()
out: list[Path] = []
for path in paths:
key = str(path)
if key in seen:
continue
seen.add(key)
out.append(path)
return out
def resolve_plantuml_jar(explicit: str | None = None, *, download: bool = False) -> str | None:
"""Find a plantuml.jar: explicit path, env, bundled vendor, user cache; optionally download."""
for path in iter_plantuml_candidates(explicit):
if path.is_file() and path.stat().st_size > 1000:
return str(path)
if download:
dest = cached_plantuml_path()
if fetch_plantuml_jar(dest):
return str(dest)
return None
def fetch_plantuml_jar(dest: Path, url: str = PLANTUML_JAR_URL) -> bool:
"""Download official plantuml.jar to dest. Returns True on success."""
dest.parent.mkdir(parents=True, exist_ok=True)
tmp = dest.with_suffix(".jar.part")
try:
resp = requests.get(url, timeout=120, stream=True)
resp.raise_for_status()
size = 0
with open(tmp, "wb") as fh:
for chunk in resp.iter_content(chunk_size=65536):
if chunk:
fh.write(chunk)
size += len(chunk)
if size < 1000:
tmp.unlink(missing_ok=True)
return False
tmp.replace(dest)
_log.info("PlantUML jar: %s", dest)
return True
except Exception as exc:
_log.warning("Не удалось скачать plantuml.jar: %s", exc)
tmp.unlink(missing_ok=True)
return False return False
with tempfile.TemporaryDirectory() as tmp:
src = Path(tmp) / "diagram.puml"
src.write_text(source, encoding="utf-8") def diagram_engine_status(explicit_jar: str | None = None) -> str:
"""Human-readable status for the GUI."""
java = shutil.which("java")
jar = resolve_plantuml_jar(explicit_jar, download=False)
if java:
java_line = f"Java: есть ({java})"
else:
java_line = (
"Java: не найдена. Локальный PlantUML не запустится — "
"картинки пойдут через интернет (kroki.io)."
)
if jar:
jar_line = f"PlantUML: {jar}"
else:
jar_line = (
"PlantUML: файла нет. Нажмите «Скачать PlantUML» (нужна Java) "
"или оставьте как есть — картинки нарисует интернет (kroki.io)."
)
lines = java_line + "\n" + jar_line
try:
from .mermaid_renderer import mermaid_engine_status
lines = lines + "\n" + mermaid_engine_status()
except Exception as exc:
lines = lines + f"\nMermaid: статус недоступен ({exc})"
lines = lines + "\nIDEF0: локальный рендер (Pillow), оградка ```idef0"
try:
from .dfd import dfd_engine_status
lines = lines + "\n" + dfd_engine_status()
except Exception as exc:
lines = lines + f"\nDFD: статус недоступен ({exc})"
return lines
def _ensure_plantuml_png_scale(source: str, scale: float = DEFAULT_DIAGRAM_SCALE) -> str:
"""Inject `scale N` into PlantUML source unless the author already set scale/dpi."""
if scale <= 1:
return source
if re.search(r"(?im)^\s*scale\b", source):
return source
if re.search(r"(?im)^\s*skinparam\s+dpi\b", source):
return source
lines = source.splitlines()
if not lines:
return source
insert_at = 0
if lines[0].strip().lower().startswith("@start"):
insert_at = 1
scale_line = f"scale {scale:g}"
lines.insert(insert_at, scale_line)
return "\n".join(lines) + ("\n" if source.endswith("\n") else "")
def _is_png(data: bytes) -> bool:
return data.startswith(b"\x89PNG")
def _is_svg(data: bytes) -> bool:
head = data.lstrip()[:200].lower()
return head.startswith(b"<svg") or head.startswith(b"<?xml") or b"<svg" in head
@dataclass
class _PumlJob:
"""One PlantUML source and where its rendered file must land."""
source: str
out_path: Path
def _pick_produced(out_dir: Path, fmt: str) -> Path | None:
"""Output name follows `@startuml <name>`, so take what the jar actually wrote."""
if not out_dir.is_dir():
return None
files = [
p for p in out_dir.iterdir()
if p.is_file() and p.suffix.lower() == f".{fmt}"
]
if not files:
return None
# `newpage` adds name_001, name_002…; the shortest name is the first page.
files.sort(key=lambda p: (len(p.name), p.name))
return files[0]
def _plantuml_run_chunk(
chunk: list[_PumlJob],
jar: str,
fmt: Literal["png", "svg"],
done: list[bool],
offset: int,
) -> None:
with tempfile.TemporaryDirectory(prefix="md2gost-puml-") as tmp:
root = Path(tmp)
sources: list[str] = []
for i, job in enumerate(chunk):
sub = root / f"{i:04d}"
sub.mkdir()
src = sub / "src.puml"
src.write_text(job.source, encoding="utf-8")
sources.append(str(src))
cmd = [ cmd = [
"java", "-jar", jar, "java",
"-tpng", f"-Dplantuml.include.path={diagrams_dir().resolve()}",
f"-DPLANTUML_LIMIT_SIZE={PLANTUML_LIMIT_SIZE}",
"-jar", jar,
f"-t{fmt}",
"-charset", "UTF-8", "-charset", "UTF-8",
f"-I{diagrams_dir()}", # Relative -o resolves against each input's own folder, so two sources
"-o", tmp, # naming themselves `@startuml Name` cannot collide on one output file.
str(src), "-o", "out",
*sources,
] ]
timeout = min(600, 120 + 30 * (len(chunk) - 1))
try: try:
proc = subprocess.run( proc = subprocess.run(
cmd, capture_output=True, text=True, timeout=120, check=False, cmd, capture_output=True, text=True, timeout=timeout, check=False,
) )
except (OSError, subprocess.TimeoutExpired) as e: except (OSError, subprocess.TimeoutExpired) as e:
_log.warning("PlantUML jar failed: %s", e) _log.warning("PlantUML jar failed: %s", e)
return False return
produced = Path(tmp) / "diagram.png" if proc.returncode != 0:
if proc.returncode != 0 or not produced.is_file():
_log.warning("PlantUML jar error: %s", proc.stderr or proc.stdout) _log.warning("PlantUML jar error: %s", proc.stderr or proc.stdout)
return False return
shutil.copyfile(produced, out_png) for i, job in enumerate(chunk):
return True produced = _pick_produced(root / f"{i:04d}" / "out", fmt)
if produced is None:
_log.warning("PlantUML jar: %s не создан", fmt)
continue
data = produced.read_bytes()
if fmt == "png" and not _is_png(data):
continue
if fmt == "svg" and not _is_svg(data):
continue
job.out_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(produced, job.out_path)
done[offset + i] = True
def _render_kroki(source: str, diagram_type: str, base_url: str, out_png: Path) -> bool: def _plantuml_run(
url = base_url.rstrip("/") + f"/{diagram_type}/png" jobs: list[_PumlJob],
jar: str,
fmt: Literal["png", "svg"],
) -> list[bool]:
"""Render every job in one JVM per chunk. Returns per-job success flags."""
if not jobs:
return []
if not jar or not os.path.isfile(jar) or not _java_available():
return [False] * len(jobs)
done = [False] * len(jobs)
for start in range(0, len(jobs), PLANTUML_BATCH_CHUNK):
_plantuml_run_chunk(
jobs[start:start + PLANTUML_BATCH_CHUNK], jar, fmt, done, start,
)
return done
def _render_plantuml_jar_one(
source: str,
out_path: Path,
jar: str,
fmt: Literal["png", "svg"],
) -> bool:
done = _plantuml_run([_PumlJob(source, Path(out_path))], jar, fmt)
return bool(done and done[0])
def _render_plantuml_jar(
source: str,
out_png: Path,
jar: str,
*,
out_svg: Path | None = None,
) -> bool:
"""Render PNG; optionally also SVG from the same jar."""
if not _render_plantuml_jar_one(source, out_png, jar, "png"):
return False
if out_svg is not None:
if not _render_plantuml_jar_one(source, out_svg, jar, "svg"):
_log.warning("PlantUML jar: PNG ok, SVG failed")
return True
def _plantuml_scaled(prepared: str, scale: float) -> tuple[str, float]:
"""Return (source, pixel_scale); pixel_scale stays 1 when the author set scale."""
if scale <= 1:
return prepared, 1.0
boosted = _ensure_plantuml_png_scale(prepared, scale)
return boosted, (scale if boosted != prepared else 1.0)
# Fences seen while parsing, so the whole document can share one JVM.
_PENDING: list[tuple[str, str]] = []
_PENDING_SEEN: set[tuple[str, str]] = set()
_BATCH_FLUSHED = False
def register_pending_diagram(lang: str, source: str) -> None:
"""Announce a diagram fence found during parsing (see _flush_plantuml_batch)."""
key = (lang or "", source or "")
if key in _PENDING_SEEN:
return
_PENDING_SEEN.add(key)
_PENDING.append(key)
def reset_diagram_batch() -> None:
global _BATCH_FLUSHED
_PENDING.clear()
_PENDING_SEEN.clear()
_BATCH_FLUSHED = False
def _flush_plantuml_batch() -> None:
"""
Pre-pass before the first diagram renders: draw every cache-missing PlantUML
source of the document in one JVM instead of one (two in svg mode) per diagram.
Anything that fails here simply stays a cache miss and takes the normal path.
"""
global _BATCH_FLUSHED
if _BATCH_FLUSHED:
return
_BATCH_FLUSHED = True
pending = list(_PENDING)
_PENDING.clear()
_PENDING_SEEN.clear()
if len(pending) < 2:
return
jar = resolve_plantuml_jar(_CONFIG.plantuml_jar or None, download=False)
if not jar or not _java_available():
return
want_svg = _CONFIG.diagram_format == "svg"
scale = _CONFIG.diagram_scale
png_jobs: list[_PumlJob] = []
svg_jobs: list[_PumlJob] = []
seen: set[str] = set()
for lang, source in pending:
try:
prepared, kroki_type = prepare_source(lang, source)
except Exception:
continue
if kroki_type != "plantuml":
continue
prepared, _ = _plantuml_scaled(prepared, scale)
out_png, out_svg = _cache_paths(prepared, cache_prefix=PLANTUML_CACHE_PREFIX)
if str(out_png) in seen:
continue
seen.add(str(out_png))
if not (out_png.is_file() and out_png.stat().st_size > 0):
png_jobs.append(_PumlJob(prepared, out_png))
if want_svg and not (out_svg.is_file() and out_svg.stat().st_size > 0):
svg_jobs.append(_PumlJob(prepared, out_svg))
if not png_jobs and not svg_jobs:
return
_log.info(
"PlantUML: пакетный рендер %d диаграмм одной JVM",
max(len(png_jobs), len(svg_jobs)),
)
if png_jobs:
_plantuml_run(png_jobs, jar, "png")
if svg_jobs:
_plantuml_run(svg_jobs, jar, "svg")
def shutdown_diagrams() -> None:
"""Release per-conversion resources: headless browser and the batch queue."""
reset_diagram_batch()
try:
from .mermaid_renderer import reset_mermaid_session
reset_mermaid_session()
except Exception:
pass
def _render_kroki(
source: str,
diagram_type: str,
base_url: str,
out_path: Path,
fmt: Literal["png", "svg"] = "png",
) -> bool:
url = base_url.rstrip("/") + f"/{diagram_type}/{fmt}"
try: try:
resp = requests.post( resp = requests.post(
url, url,
@@ -143,16 +534,38 @@ def _render_kroki(source: str, diagram_type: str, base_url: str, out_png: Path)
headers={"Content-Type": "text/plain"}, headers={"Content-Type": "text/plain"},
timeout=60, timeout=60,
) )
if resp.status_code != 200 or not resp.content.startswith(b"\x89PNG"): if resp.status_code != 200:
_log.warning("Kroki %s → HTTP %s", url, resp.status_code) _log.warning("Kroki %s → HTTP %s", url, resp.status_code)
return False return False
out_png.write_bytes(resp.content) data = resp.content
if fmt == "png" and not _is_png(data):
_log.warning("Kroki %s → not PNG", url)
return False
if fmt == "svg" and not _is_svg(data):
_log.warning("Kroki %s → not SVG", url)
return False
out_path.write_bytes(data)
return True return True
except requests.RequestException as e: except requests.RequestException as e:
_log.warning("Kroki request failed (%s): %s", url, e) _log.warning("Kroki request failed (%s): %s", url, e)
return False return False
def _render_kroki_pair(
source: str,
diagram_type: str,
base_url: str,
out_png: Path,
out_svg: Path | None,
) -> bool:
if not _render_kroki(source, diagram_type, base_url, out_png, "png"):
return False
if out_svg is not None:
if not _render_kroki(source, diagram_type, base_url, out_svg, "svg"):
_log.warning("Kroki: PNG ok, SVG failed (%s)", base_url)
return True
def render_diagram( def render_diagram(
lang: str, lang: str,
source: str, source: str,
@@ -161,36 +574,212 @@ def render_diagram(
kroki_url: str | None = None, kroki_url: str | None = None,
fallback: FallbackMode | None = None, fallback: FallbackMode | None = None,
cache_dir: str | None = None, cache_dir: str | None = None,
) -> str: diagram_format: DiagramFormat | None = None,
"""Render diagram to PNG path. Raises RuntimeError if all backends fail.""" diagram_scale: float | None = None,
) -> DiagramRenderResult:
"""Render diagram to PNG (+ optional SVG). Raises RuntimeError if all backends fail."""
# Batch only the plain document path; explicit overrides would not match _CONFIG.
if not any(
arg is not None
for arg in (
plantuml_jar, kroki_url, fallback, cache_dir, diagram_format, diagram_scale,
)
):
try:
_flush_plantuml_batch()
except Exception as exc:
_log.warning("PlantUML: пакетный рендер не удался (%s)", exc)
prepared, kroki_type = prepare_source(lang, source) prepared, kroki_type = prepare_source(lang, source)
out = _cache_path(prepared, cache_dir) scale = _CONFIG.diagram_scale if diagram_scale is None else float(diagram_scale)
if out.is_file() and out.stat().st_size > 0: if scale < 1:
return str(out) scale = 1.0
# pixel_scale: only when we injected scale (author override → treat as 1 for display).
pixel_scale = 1.0
if kroki_type == "plantuml":
prepared, pixel_scale = _plantuml_scaled(prepared, scale)
# Mermaid: pixel_scale set only after successful local hi-res render (not Kroki).
cache_prefix = ""
if kroki_type == "plantuml":
cache_prefix = PLANTUML_CACHE_PREFIX
elif kroki_type == "mermaid":
try:
from .mermaid_renderer import CACHE_KEY_PREFIX
cache_prefix = CACHE_KEY_PREFIX
except Exception:
cache_prefix = "mmd-local-v4\n"
elif kroki_type == "idef0":
from .idef0 import CACHE_KEY_PREFIX as IDEF0_CACHE_PREFIX
cache_prefix = IDEF0_CACHE_PREFIX
elif kroki_type == "dfd":
from .dfd import CACHE_KEY_PREFIX as DFD_CACHE_PREFIX
cache_prefix = DFD_CACHE_PREFIX
# Local hi-res uses a scale-specific cache key; Kroki uses unscaled key.
local_prefix = cache_prefix
if kroki_type in SCALED_LOCAL_TYPES and scale > 1:
local_prefix = f"{cache_prefix}scale={scale:g}\n"
out_png, out_svg_path = _cache_paths(
prepared, cache_dir,
cache_prefix=local_prefix if kroki_type in SCALED_LOCAL_TYPES else cache_prefix,
)
# Kroki / remote paths (mermaid): never share the scaled local key.
kroki_png, kroki_svg_path = out_png, out_svg_path
if kroki_type == "mermaid":
kroki_png, kroki_svg_path = _cache_paths(
prepared, cache_dir, cache_prefix=cache_prefix,
)
fmt: DiagramFormat = (
diagram_format
if diagram_format is not None
else _CONFIG.diagram_format
)
want_svg = fmt == "svg"
svg_target = out_svg_path if want_svg else None
kroki_svg_target = kroki_svg_path if want_svg else None
if out_png.is_file() and out_png.stat().st_size > 0:
has_svg = out_svg_path.is_file() and out_svg_path.stat().st_size > 0
if not want_svg or has_svg:
hit_scale = pixel_scale
if kroki_type in SCALED_LOCAL_TYPES and scale > 1 and local_prefix != cache_prefix:
hit_scale = scale
return DiagramRenderResult(
png_path=str(out_png),
svg_path=str(out_svg_path) if (want_svg and has_svg) else None,
pixel_scale=hit_scale,
)
# PNG cached but SVG missing in svg mode — fall through to fill SVG.
# Mermaid: also accept unscaled Kroki cache hit
if (
kroki_type == "mermaid"
and kroki_png != out_png
and kroki_png.is_file()
and kroki_png.stat().st_size > 0
):
has_svg = kroki_svg_path.is_file() and kroki_svg_path.stat().st_size > 0
if not want_svg or has_svg:
return DiagramRenderResult(
png_path=str(kroki_png),
svg_path=str(kroki_svg_path) if (want_svg and has_svg) else None,
pixel_scale=1.0,
)
jar = plantuml_jar if plantuml_jar is not None else _CONFIG.plantuml_jar jar = plantuml_jar if plantuml_jar is not None else _CONFIG.plantuml_jar
jar = jar or os.environ.get("PLANTUML_JAR") jar = resolve_plantuml_jar(jar or None, download=False)
local_kroki = kroki_url if kroki_url is not None else _CONFIG.kroki_url local_kroki = kroki_url if kroki_url is not None else _CONFIG.kroki_url
local_kroki = local_kroki or os.environ.get("KROKI_URL") or DEFAULT_KROKI_URL local_kroki = local_kroki or os.environ.get("KROKI_URL") or DEFAULT_KROKI_URL
mode: FallbackMode = fallback if fallback is not None else _CONFIG.fallback mode: FallbackMode = fallback if fallback is not None else _CONFIG.fallback
# 1) PlantUML jar def _result(
if _render_plantuml_jar(prepared, out, jar or ""): png: Path | None = None,
return str(out) svg_path: Path | None = None,
*,
pscale: float | None = None,
) -> DiagramRenderResult:
use_png = png or out_png
use_svg_path = svg_path if svg_path is not None else out_svg_path
svg = None
if want_svg and use_svg_path.is_file() and use_svg_path.stat().st_size > 0:
svg = str(use_svg_path)
return DiagramRenderResult(
png_path=str(use_png),
svg_path=svg,
pixel_scale=pixel_scale if pscale is None else pscale,
)
# 1) PlantUML jar — only for plantuml type
if kroki_type == "plantuml":
if _render_plantuml_jar(prepared, out_png, jar or "", out_svg=svg_target):
return _result()
# 1a) Native Pillow (IDEF0 / DFD) — never Kroki / PlantUML
if kroki_type in LOCAL_ONLY_TYPES:
if kroki_type == "idef0":
from .idef0 import Idef0ParseError, write_idef0_outputs
try:
if write_idef0_outputs(prepared, out_png, svg_target, scale=scale):
return _result(pscale=scale if scale > 1 else 1.0)
except Idef0ParseError as exc:
raise RuntimeError(str(exc)) from exc
raise RuntimeError("Не удалось отрендерить IDEF0-диаграмму")
if kroki_type == "dfd":
from .dfd import DfdParseError, write_dfd_outputs
try:
if write_dfd_outputs(prepared, out_png, svg_target, scale=scale):
return _result(pscale=scale if scale > 1 else 1.0)
except DfdParseError as exc:
raise RuntimeError(str(exc)) from exc
raise RuntimeError("Не удалось отрендерить DFD-диаграмму")
raise RuntimeError(f"Неизвестный локальный тип диаграммы: {kroki_type}")
# 1b) Mermaid local: system browser → Playwright Chromium → QuickJS
if kroki_type == "mermaid":
try:
from .mermaid_renderer import write_mermaid_outputs
if write_mermaid_outputs(
prepared, out_png, svg_target, scale=scale,
):
return _result(pscale=scale if scale > 1 else 1.0)
except Exception as exc:
_log.warning("Mermaid local render failed: %s", exc)
# 2) Local Kroki # 2) Local Kroki
if _render_kroki(prepared, kroki_type, local_kroki, out): if _render_kroki_pair(prepared, kroki_type, local_kroki, kroki_png, kroki_svg_target):
return str(out) if kroki_type == "mermaid":
return _result(kroki_png, kroki_svg_path, pscale=1.0)
return _result(kroki_png, kroki_svg_path)
def _png_fallback_or_raise(message: str) -> DiagramRenderResult:
for candidate in (out_png, kroki_png):
if candidate.is_file() and candidate.stat().st_size > 0:
_log.warning("Диаграмма: SVG недоступен, вставляю только PNG")
pscale = pixel_scale
if (
kroki_type == "mermaid"
and candidate == out_png
and scale > 1
and local_prefix != cache_prefix
):
pscale = scale
elif kroki_type == "mermaid" and candidate == kroki_png:
pscale = 1.0
return DiagramRenderResult(
png_path=str(candidate), svg_path=None, pixel_scale=pscale,
)
raise RuntimeError(message)
if mode == "off": if mode == "off":
raise RuntimeError( if kroki_type == "plantuml":
return _png_fallback_or_raise(
"Не удалось отрендерить диаграмму локально "
"(задайте --plantuml-jar или KROKI_URL; remote fallback отключён)"
)
return _png_fallback_or_raise(
"Не удалось отрендерить диаграмму локально " "Не удалось отрендерить диаграмму локально "
"(задайте --plantuml-jar или KROKI_URL; remote fallback отключён)" "(браузер / QuickJS / --kroki-url; remote fallback отключён)"
) )
if mode == "local": if mode == "local":
raise RuntimeError( if kroki_type == "plantuml":
"Локальный рендер диаграммы недоступен " return _png_fallback_or_raise(
"(Java+plantuml.jar или локальный Kroki)" "Локальный рендер диаграммы недоступен "
"(Java+plantuml.jar или локальный Kroki)"
)
return _png_fallback_or_raise(
"Локальный рендер Mermaid недоступен "
f"(браузер / Chromium / QuickJS / Kroki; тип {kroki_type})"
) )
# 3) Remote fallback # 3) Remote fallback
@@ -198,7 +787,32 @@ def render_diagram(
"Диаграмма: локальный рендер недоступен, использую remote %s", "Диаграмма: локальный рендер недоступен, использую remote %s",
REMOTE_KROKI_URL, REMOTE_KROKI_URL,
) )
if _render_kroki(prepared, kroki_type, REMOTE_KROKI_URL, out): if _render_kroki_pair(prepared, kroki_type, REMOTE_KROKI_URL, kroki_png, kroki_svg_target):
return str(out) if kroki_type == "mermaid":
return _result(kroki_png, kroki_svg_path, pscale=1.0)
return _result(kroki_png, kroki_svg_path)
raise RuntimeError("Не удалось отрендерить диаграмму (PlantUML/Kroki)") return _png_fallback_or_raise(
"Не удалось отрендерить диаграмму (PlantUML/Mermaid/Kroki)"
)
# Re-export for callers / tests
__all__ = [
"DIAGRAM_LANGS",
"DiagramRenderResult",
"configure_diagrams",
"diagram_engine_status",
"diagrams_dir",
"fetch_plantuml_jar",
"is_diagram_lang",
"prepare_source",
"register_pending_diagram",
"render_diagram",
"reset_diagram_batch",
"resolve_plantuml_jar",
"shutdown_diagrams",
"PLANTUML_CACHE_PREFIX",
"PLANTUML_JAR_URL",
"PLANTUML_VERSION",
]
+462
View File
@@ -0,0 +1,462 @@
"""Configurable PlantUML diagram schemes (uml-c4, …) from md2gost.schemes.json."""
from __future__ import annotations
import json
import logging
import re
import shutil
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
from . import package_dir
from .diagram_includes import (
app_dir,
resolve_include_ref,
rewrite_http_includes_in_source,
)
_log = logging.getLogger(__name__)
SCHEMES_FILENAME = "md2gost.schemes.json"
SCHEME_ID_RE = re.compile(r"^[a-z][a-z0-9_]*$")
APP_SCHEME_AUTHOR = "md2gost"
BUILTIN_DIAGRAM_LANGS = frozenset({"uml", "plantuml"})
# Rendered via Kroki only (not PlantUML jar / schemes).
KROKI_DIAGRAM_LANGS = frozenset({"mermaid", "mmd"})
KROKI_TYPE_BY_LANG = {"mermaid": "mermaid", "mmd": "mermaid"}
# Local native renderers (not PlantUML / Kroki / schemes.json).
NATIVE_DIAGRAM_LANGS = {
"idef0": "idef0",
"uml-idef0": "idef0",
"dfd": "dfd",
"uml-dfd": "dfd",
"data-flow-diagram": "dfd",
"yourdon": "dfd",
"uml-yourdon": "dfd",
}
@dataclass
class SchemesMigrationResult:
added: list[str] = field(default_factory=list)
updated: list[str] = field(default_factory=list)
removed: list[str] = field(default_factory=list)
written: bool = False
@property
def changed(self) -> bool:
return bool(self.added or self.updated or self.removed)
@dataclass
class DiagramScheme:
id: str
title: str = ""
version: str = ""
author: str = ""
docs: str = ""
ai_prompt: str = ""
includes: list[str] = field(default_factory=list)
prefix: str = ""
postfix: str = ""
theme: str = ""
def to_dict(self) -> dict[str, Any]:
data: dict[str, Any] = {
"title": self.title,
"version": self.version,
"author": self.author,
"docs": self.docs,
"ai-prompt": self.ai_prompt,
"includes": list(self.includes),
"prefix": self.prefix,
"postfix": self.postfix,
}
if self.theme:
data["theme"] = self.theme
return data
@classmethod
def from_dict(cls, scheme_id: str, data: dict[str, Any]) -> DiagramScheme:
includes = data.get("includes") or []
if isinstance(includes, str):
includes = [includes]
return cls(
id=scheme_id,
title=str(data.get("title") or scheme_id),
version=str(data.get("version") or ""),
author=str(data.get("author") or ""),
docs=str(data.get("docs") or ""),
ai_prompt=str(data.get("ai-prompt") or data.get("ai_prompt") or ""),
includes=[str(x) for x in includes],
prefix=str(data.get("prefix") or ""),
postfix=str(data.get("postfix") or ""),
theme=str(data.get("theme") or ""),
)
def bundled_schemes_path() -> Path:
return Path(package_dir()) / "diagrams" / "schemes.json"
def user_schemes_path(base: Path | None = None) -> Path:
return (base or app_dir()) / SCHEMES_FILENAME
def is_app_owned_scheme(scheme: DiagramScheme) -> bool:
"""True if scheme is managed by the app template (author=md2gost)."""
return scheme.author.strip().lower() == APP_SCHEME_AUTHOR
def _schemes_payload(schemes: dict[str, DiagramScheme]) -> dict[str, Any]:
return {sid: scheme.to_dict() for sid, scheme in sorted(schemes.items())}
def migrate_user_schemes(base: Path | None = None) -> SchemesMigrationResult:
"""
Sync app-owned schemes in the user file with the bundled template.
- Bundled ids: always present; overwrite user entries with author=md2gost.
- User overrides of bundled ids (empty/other author): kept as-is.
- Orphaned author=md2gost ids no longer in the template: removed.
- Other custom schemes: kept.
"""
result = SchemesMigrationResult()
path = user_schemes_path(base)
if not path.is_file():
return result
bundled = _load_schemes_file(bundled_schemes_path())
user = _load_schemes_file(path)
out: dict[str, DiagramScheme] = {}
for sid, scheme in user.items():
if sid in bundled:
if is_app_owned_scheme(scheme):
continue # step below writes bundled
out[sid] = scheme
continue
if is_app_owned_scheme(scheme):
result.removed.append(sid)
_log.info("Миграция схем: удалена устаревшая встроенная «%s»", sid)
continue
out[sid] = scheme
for sid, scheme in bundled.items():
if sid in out:
continue # user-owned override
prev = user.get(sid)
if prev is None:
result.added.append(sid)
_log.info("Миграция схем: добавлена «%s»", sid)
elif prev.to_dict() != scheme.to_dict():
result.updated.append(sid)
_log.info("Миграция схем: обновлена «%s»", sid)
out[sid] = scheme
if _schemes_payload(out) == _schemes_payload(user):
result.added.clear()
result.updated.clear()
result.removed.clear()
return result
save_schemes(out, path)
result.written = True
return result
def ensure_user_schemes(base: Path | None = None) -> Path:
"""
Ensure md2gost.schemes.json exists next to the app, then sync app-owned
schemes from the bundled template (add / update / remove orphans).
"""
dest = user_schemes_path(base)
if not dest.is_file():
src = bundled_schemes_path()
dest.parent.mkdir(parents=True, exist_ok=True)
if src.is_file():
shutil.copyfile(src, dest)
_log.info("Создан файл схем: %s", dest)
else:
dest.write_text("{}\n", encoding="utf-8")
_log.warning("Шаблон схем не найден (%s), создан пустой %s", src, dest)
migrate_user_schemes(base)
return dest
def _load_schemes_file(path: Path) -> dict[str, DiagramScheme]:
if not path.is_file():
return {}
try:
raw = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
_log.warning("Не удалось прочитать схемы %s: %s", path, exc)
return {}
if not isinstance(raw, dict):
return {}
out: dict[str, DiagramScheme] = {}
for key, val in raw.items():
sid = str(key).lower().strip()
if not SCHEME_ID_RE.match(sid):
_log.warning("Пропуск схемы с недопустимым id: %s", key)
continue
if not isinstance(val, dict):
continue
out[sid] = DiagramScheme.from_dict(sid, val)
return out
def load_schemes_from_path(path: Path) -> dict[str, DiagramScheme]:
return _load_schemes_file(path)
def save_schemes(schemes: dict[str, DiagramScheme], path: Path | None = None) -> Path:
dest = path or user_schemes_path()
payload = _schemes_payload(schemes)
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_text(
json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
return dest
def claim_user_scheme_if_diverged(
scheme: DiagramScheme,
*,
bundled: dict[str, DiagramScheme] | None = None,
) -> DiagramScheme:
"""
If scheme id is in the app template and content differs while still
author=md2gost, clear author so migration will not overwrite user edits.
"""
stock = (bundled if bundled is not None else _load_schemes_file(bundled_schemes_path())).get(
scheme.id
)
if stock is None:
return scheme
if not is_app_owned_scheme(scheme):
return scheme
if scheme.to_dict() == stock.to_dict():
return scheme
return DiagramScheme(
id=scheme.id,
title=scheme.title,
version=scheme.version,
author="",
docs=scheme.docs,
ai_prompt=scheme.ai_prompt,
includes=list(scheme.includes),
prefix=scheme.prefix,
postfix=scheme.postfix,
theme=scheme.theme,
)
def native_diagram_type(lang: str) -> str | None:
"""Kroki/PlantUML-independent diagram type, or None."""
lang = (lang or "").lower().strip()
return NATIVE_DIAGRAM_LANGS.get(lang)
def scheme_id_from_lang(lang: str) -> str | None:
"""Normalize fence lang to scheme id, or None for bare uml/plantuml."""
lang = (lang or "").lower().strip()
if not lang:
return None
if lang in BUILTIN_DIAGRAM_LANGS or lang in KROKI_DIAGRAM_LANGS or lang in NATIVE_DIAGRAM_LANGS:
return None
if lang.startswith("uml-"):
sid = lang[4:]
return sid if SCHEME_ID_RE.match(sid) else None
if SCHEME_ID_RE.match(lang):
return lang
return None
def load_merged_schemes(
*,
base: Path | None = None,
extra_path: Path | None = None,
md_dir: Path | None = None,
) -> dict[str, DiagramScheme]:
"""
Merge layers (later wins entirely per id):
1) bundled template
2) user file next to app (created on first run)
3) md2gost.schemes.json next to .md
4) explicit --schemes path
"""
ensure_user_schemes(base)
merged: dict[str, DiagramScheme] = {}
merged.update(_load_schemes_file(bundled_schemes_path()))
merged.update(_load_schemes_file(user_schemes_path(base)))
if md_dir:
near_md = Path(md_dir) / SCHEMES_FILENAME
if near_md.is_file():
user = user_schemes_path(base).resolve()
try:
if near_md.resolve() != user:
merged.update(_load_schemes_file(near_md))
except OSError:
merged.update(_load_schemes_file(near_md))
if extra_path:
merged.update(_load_schemes_file(Path(extra_path)))
return merged
_SCHEMES_CACHE: dict[str, DiagramScheme] | None = None
_SCHEMES_EXTRA: Path | None = None
_SCHEMES_MD_DIR: Path | None = None
def configure_schemes(
*,
schemes_path: str | None = None,
md_dir: str | None = None,
base: Path | None = None,
) -> dict[str, DiagramScheme]:
global _SCHEMES_CACHE, _SCHEMES_EXTRA, _SCHEMES_MD_DIR
_SCHEMES_EXTRA = Path(schemes_path) if schemes_path else None
_SCHEMES_MD_DIR = Path(md_dir) if md_dir else None
_SCHEMES_CACHE = load_merged_schemes(
base=base,
extra_path=_SCHEMES_EXTRA,
md_dir=_SCHEMES_MD_DIR,
)
return _SCHEMES_CACHE
def get_schemes() -> dict[str, DiagramScheme]:
global _SCHEMES_CACHE
if _SCHEMES_CACHE is None:
_SCHEMES_CACHE = load_merged_schemes()
return _SCHEMES_CACHE
def reload_schemes(**kwargs) -> dict[str, DiagramScheme]:
return configure_schemes(**kwargs)
def get_scheme(scheme_id: str) -> DiagramScheme | None:
return get_schemes().get(scheme_id)
def is_diagram_lang(lang: str) -> bool:
"""True if fence should render as a diagram (not a plain listing)."""
lang = (lang or "").lower().strip()
if lang in BUILTIN_DIAGRAM_LANGS or lang in KROKI_DIAGRAM_LANGS or lang in NATIVE_DIAGRAM_LANGS:
return True
if lang.startswith("uml-"):
# Force diagram path so missing scheme becomes an error, not a listing.
return bool(SCHEME_ID_RE.match(lang[4:]))
return lang in get_schemes()
def apply_scheme(
scheme: DiagramScheme,
body: str,
*,
base: Path | None = None,
) -> str:
"""Wrap body with includes / prefix / postfix / theme; resolve URL includes."""
text = body.strip()
has_start = text.lower().startswith("@start")
include_lines: list[str] = []
schemes_dir = user_schemes_path(base).parent
for ref in scheme.includes:
local = resolve_include_ref(ref, base=base, schemes_dir=schemes_dir)
include_lines.append(f"!include {local}")
theme_line = ""
if scheme.theme and "!theme" not in text.lower():
t = scheme.theme.strip()
if not t.lower().startswith("!theme"):
t = f"!theme {t}"
theme_line = t
prefix = scheme.prefix or ""
postfix = scheme.postfix or ""
# Avoid double @startuml / @enduml when body already has @start…
if has_start:
prefix_use = _strip_start_end_wrappers(prefix)
postfix_use = _strip_start_end_wrappers(postfix)
head_bits = [ln for ln in include_lines if ln]
if theme_line:
head_bits.append(theme_line)
if prefix_use.strip():
head_bits.append(prefix_use.strip())
if head_bits:
lines = text.splitlines()
text = lines[0] + "\n" + "\n".join(head_bits) + "\n" + "\n".join(lines[1:])
if postfix_use.strip():
text = text.rstrip() + "\n" + postfix_use.strip()
else:
parts: list[str] = []
p = prefix.rstrip("\n") if prefix else ""
pf = postfix.lstrip("\n") if postfix else ""
if not p.lstrip().lower().startswith("@start"):
parts.append("@startuml")
if p:
parts.append(p)
parts.extend(include_lines)
if theme_line:
parts.append(theme_line)
parts.append(text)
if pf:
parts.append(pf)
elif "@enduml" not in text.lower():
parts.append("@enduml")
text = "\n".join(parts)
text = rewrite_http_includes_in_source(text, base=base)
return text
def _strip_start_end_wrappers(chunk: str) -> str:
"""Remove leading @start… and trailing @end… lines from prefix/postfix."""
lines = chunk.splitlines()
while lines and lines[0].strip().lower().startswith("@start"):
lines = lines[1:]
while lines and lines[-1].strip().lower().startswith("@end"):
lines = lines[:-1]
return "\n".join(lines)
def prepare_with_schemes(lang: str, source: str, *, base: Path | None = None) -> tuple[str, str]:
"""
Prepare diagram source using schemes.
Returns (prepared_source, kroki_diagram_type).
Raises ValueError if uml-<id> / named scheme is missing.
"""
lang = (lang or "uml").lower().strip()
text = source.strip()
if lang in KROKI_DIAGRAM_LANGS:
return text, KROKI_TYPE_BY_LANG[lang]
native = native_diagram_type(lang)
if native:
return text, native
sid = scheme_id_from_lang(lang)
if sid is None:
# bare uml / plantuml
if not text.lower().startswith("@start"):
text = f"@startuml\n{text}\n@enduml"
text = rewrite_http_includes_in_source(text, base=base)
return text, "plantuml"
scheme = get_scheme(sid)
if scheme is None:
raise ValueError(
f"Схема диаграммы «{sid}» не найдена "
f"(оградка ```{lang}). Добавьте её в {SCHEMES_FILENAME}."
)
prepared = apply_scheme(scheme, text, base=base)
return prepared, "plantuml"
+2314
View File
File diff suppressed because one or more lines are too long
+84
View File
@@ -0,0 +1,84 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<title>md2gost mermaid</title>
<script src="mermaid.min.js"></script>
<style>
html, body { margin: 0; padding: 0; background: #fff; }
#out { display: inline-block; }
</style>
</head>
<body>
<div id="out"></div>
<script>
// Browser: htmlLabels:true — корректные боксы под кириллицу; PNG = screenshot.
mermaid.initialize({
startOnLoad: false,
securityLevel: "loose",
flowchart: {
htmlLabels: true,
wrappingWidth: 320,
subGraphTitleMargin: { top: 12, bottom: 16 },
padding: 20,
},
themeVariables: {
fontFamily: '"Segoe UI", "DejaVu Sans", Arial, sans-serif',
},
});
/** Put subgraph titles above node rows and paint them on top (Mermaid LR quirk). */
function fixClusterLabels(root) {
root.querySelectorAll("g.cluster").forEach((cluster) => {
const label = cluster.querySelector(".cluster-label");
const rect = cluster.querySelector(":scope > rect, rect");
if (!label || !rect) return;
const fo = label.querySelector("foreignObject");
const rw = parseFloat(rect.getAttribute("width") || "0");
const rx = parseFloat(rect.getAttribute("x") || "0");
const ry = parseFloat(rect.getAttribute("y") || "0");
if (fo && rw > 0) {
const maxW = Math.max(160, rw - 24);
fo.setAttribute("width", String(maxW));
const inner = fo.querySelector("div");
if (inner) {
inner.style.maxWidth = maxW + "px";
inner.style.width = maxW + "px";
inner.style.whiteSpace = "normal";
}
}
let labelH = 28;
try {
const bb = label.getBBox();
if (bb && bb.height > 0) labelH = bb.height;
else if (fo) labelH = parseFloat(fo.getAttribute("height") || "48") || 48;
} catch (e) {
if (fo) labelH = parseFloat(fo.getAttribute("height") || "48") || 48;
}
let labelW = 200;
try {
const bb = label.getBBox();
if (bb && bb.width > 0) labelW = bb.width;
else if (fo) labelW = parseFloat(fo.getAttribute("width") || "200") || 200;
} catch (e) {
if (fo) labelW = parseFloat(fo.getAttribute("width") || "200") || 200;
}
const x = rx + Math.max(0, (rw - labelW) / 2);
const y = ry + 6;
label.setAttribute("transform", "translate(" + x + ", " + y + ")");
cluster.appendChild(label);
});
}
window.__md2gostRender = async function (code) {
const id = "m" + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
const out = await mermaid.render(id, code);
const box = document.getElementById("out");
box.innerHTML = out.svg;
fixClusterLabels(box);
const svg = box.querySelector("svg");
return svg ? svg.outerHTML : out.svg;
};
</script>
</body>
</html>
+58
View File
@@ -0,0 +1,58 @@
{
"c4": {
"title": "C4 Container",
"version": "1.0.0",
"author": "md2gost",
"docs": "Person(alias, \"Label\", \"descr?\")\nSystem(alias, \"Label\", \"descr?\")\nSystem_Ext(alias, \"Label\")\nContainer(alias, \"Label\", \"techn\", \"descr?\")\nContainerDb(alias, \"Label\", \"techn\")\nRel(from, to, \"label\", \"techn?\")\nRel_R / Rel_L / Rel_U / Rel_D — направление\nLAYOUT_WITH_LEGEND()",
"ai-prompt": "Рисуй C4 Container diagram. В блоке ```uml-c4 (или ```c4) пиши только макросы C4: Person, System, Container, Rel. Без @startuml и без !include — схема добавит сама.",
"includes": [
"https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml"
],
"prefix": "@startuml\n",
"postfix": "\nLAYOUT_WITH_LEGEND()\n@enduml"
},
"c4context": {
"title": "C4 Context",
"version": "1.0.0",
"author": "md2gost",
"docs": "Person(alias, \"Label\")\nSystem(alias, \"Label\", \"descr?\")\nSystem_Ext(alias, \"Label\")\nSystem_Boundary(alias, \"Label\") { … }\nRel(from, to, \"label\")\nLAYOUT_WITH_LEGEND()",
"ai-prompt": "Рисуй C4 System Context. В блоке ```uml-c4context только макросы C4 Context. Без @startuml и !include.",
"includes": [
"https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Context.puml"
],
"prefix": "@startuml\n",
"postfix": "\nLAYOUT_WITH_LEGEND()\n@enduml"
},
"c4component": {
"title": "C4 Component",
"version": "1.0.0",
"author": "md2gost",
"docs": "Container_Boundary(alias, \"Label\") { … }\nComponent(alias, \"Label\", \"techn\", \"descr?\")\nComponentDb(alias, \"Label\", \"techn\")\nRel(from, to, \"label\")\nLAYOUT_WITH_LEGEND()",
"ai-prompt": "Рисуй C4 Component diagram. В блоке ```uml-c4component только макросы C4 Component. Без @startuml и !include.",
"includes": [
"https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Component.puml"
],
"prefix": "@startuml\n",
"postfix": "\nLAYOUT_WITH_LEGEND()\n@enduml"
},
"usecase": {
"title": "Прецеденты (Use Case)",
"version": "1.0.0",
"author": "md2gost",
"docs": "left to right direction\nactor \"Имя\" as A\nrectangle Система {\n usecase \"Сценарий\" as UC1\n}\nA --> UC1\nUC1 .> UC2 : <<include>>\nUC3 .> UC1 : <<extend>>",
"ai-prompt": "Рисуй UML Use Case. В блоке ```uml-usecase (или ```usecase) — actor, usecase, связи. Без @startuml — схема обернёт сама.",
"includes": [],
"prefix": "@startuml\nleft to right direction\nskinparam actorStyle awesome\n",
"postfix": "\n@enduml"
},
"archimate": {
"title": "ArchiMate 3.2",
"version": "1.0.0",
"author": "md2gost",
"docs": "LAYOUT_TOP_DOWN() / LAYOUT_LEFT_RIGHT()\nMotivation_Stakeholder/Driver/Assessment/Goal/Outcome/Principle/Requirement/Constraint/Meaning/Value\nStrategy_Resource/Capability/ValueStream/CourseOfAction\nBusiness_Actor/Role/Collaboration/Interface/Process/Function/Interaction/Event/Service/Object/Contract/Representation/Product\nApplication_Component/Collaboration/Interface/Function/Interaction/Process/Event/Service/DataObject\nTechnology_Node/Device/SystemSoftware/Collaboration/Interface/Path/CommunicationNetwork/Function/Process/Interaction/Event/Service/Artifact\nPhysical_Equipment/Facility/DistributionNetwork/Material\nImplementation_WorkPackage/Deliverable/Event/Plateau/Gap\nGrouping / Other_Grouping / Group / Other_Location / Junction_And / Junction_Or\nRel_Composition/Aggregation/Assignment/Realization/Serving/Access(_r|_w|_rw)/Influence/Triggering/Flow/Specialization/Association(_dir)\nRel_Type_Up/Down/Left/Right(from, to, \"label?\")\nLay_U/D/L/R — скрытое выравнивание",
"ai-prompt": "Рисуй ArchiMate 3.2 (Open Group C226). В блоке ```uml-archimate (или ```archimate) только макросы Archimate-PlantUML. Без @startuml и без !include — схема добавит сама.\n\nПравила: один рисунок = один viewpoint. Поведение по слоям (Business_Process ≠ Application_Process ≠ Technology_Process). Цепочка: Active —Assignment→ Process/Function —Realization→ Service —Serving→ потребитель; Access к пассивному объекту. Serving от поставщика к потребителю; Realization от конкретного к абстрактному; Composition/Aggregation целое→часть. Не Association «на всё». Не UML/C4/BPMN. Alias латиница (не end/start/group). 8–18 элементов. Сначала элементы, потом Rel_*. Подписи по-русски.\n\nМакросы: Motivation_*, Strategy_*, Business_*, Application_*, Technology_*, Physical_*, Implementation_*, Grouping, Junction_And/Or. Связи: Rel_Composition/Aggregation/Assignment/Realization/Serving/Access_r|w|rw/Influence/Triggering/Flow/Specialization/Association + _Up/_Down/_Left/_Right. Макет: LAYOUT_TOP_DOWN() или LAYOUT_LEFT_RIGHT(). Полный гайд: docs/archimate.md.",
"includes": [],
"prefix": "@startuml\n!include <archimate/Archimate>\n!theme archimate-standard from <archimate/themes>\n",
"postfix": "\n@enduml"
}
}
+333
View File
@@ -0,0 +1,333 @@
"""File-drop helpers for the md2gost GUI (tkinterdnd2 or Windows WM_DROPFILES)."""
from __future__ import annotations
import os
import sys
from collections.abc import Callable
from urllib.parse import unquote, urlparse
DropCallback = Callable[[list[str]], None]
def parse_tkdnd_files(data: str) -> list[str]:
"""Parse a TkDND file list (`{C:\\a b.md} C:\\c.md`)."""
files: list[str] = []
text = (data or "").strip()
i = 0
n = len(text)
while i < n:
if text[i].isspace():
i += 1
continue
if text[i] == "{":
j = text.find("}", i + 1)
if j < 0:
files.append(text[i + 1 :])
break
files.append(text[i + 1 : j])
i = j + 1
continue
j = i
while j < n and not text[j].isspace():
j += 1
files.append(text[i:j])
i = j
return files
def normalize_drop_paths(items: list[str] | str) -> list[str]:
if isinstance(items, str):
items = parse_tkdnd_files(items)
out: list[str] = []
for raw in items:
if isinstance(raw, bytes):
try:
raw = raw.decode("utf-8")
except UnicodeDecodeError:
raw = raw.decode(sys.getfilesystemencoding() or "utf-8", errors="replace")
path = raw.strip().strip('"')
if not path:
continue
if path.lower().startswith("file:"):
parsed = urlparse(path)
path = unquote(parsed.path)
if sys.platform == "win32" and path.startswith("/") and len(path) > 3 and path[2] == ":":
path = path[1:]
out.append(os.path.normpath(path))
return out
def first_markdown(paths: list[str]) -> str | None:
for path in paths:
if path.lower().endswith(".md") and os.path.isfile(path):
return path
return None
def first_docx(paths: list[str]) -> str | None:
for path in paths:
if path.lower().endswith(".docx") and os.path.isfile(path):
return path
return None
def enable_file_drop(widget, callback: DropCallback) -> str:
"""Enable dropping files onto widget. Returns backend name: tkdnd | win32 | none."""
def _deliver(items: list[str] | str) -> None:
callback(normalize_drop_paths(items))
if _try_tkdnd(widget, _deliver):
return "tkdnd"
if sys.platform == "win32" and _WinDropHook.attach(widget, _deliver):
return "win32"
return "none"
def _try_tkdnd(widget, deliver: DropCallback) -> bool:
try:
from tkinterdnd2 import DND_FILES
except ImportError:
return False
register = getattr(widget, "drop_target_register", None)
bind = getattr(widget, "dnd_bind", None)
if register is None or bind is None:
root = widget.winfo_toplevel()
register = getattr(root, "drop_target_register", None)
bind = getattr(root, "dnd_bind", None)
target = root
else:
target = widget
if register is None or bind is None:
return False
try:
register(DND_FILES)
bind("<<Drop>>", lambda event: deliver(getattr(event, "data", "") or ""))
bind("<<DragEnter>>", lambda event: widget.event_generate("<<Md2GostDragEnter>>"))
bind("<<DragLeave>>", lambda event: widget.event_generate("<<Md2GostDragLeave>>"))
except Exception:
return False
return True
class _WinDropHook:
"""Subclass a Win32 HWND and accept WM_DROPFILES. Keep a strong ref on the widget."""
_hooks: list[_WinDropHook] = []
def __init__(self, widget, callback: DropCallback):
self.widget = widget
self.callback = callback
self._pending: list[str] = []
self._old_proc = None
self._wndproc = None
self._hwnd = 0
self._alive = True
@classmethod
def attach(cls, widget, callback: DropCallback) -> bool:
try:
toplevel = widget.winfo_toplevel()
except Exception:
toplevel = widget
if getattr(toplevel, "_md2gost_dnd_hooked", False):
return True
hook = cls(widget, callback)
def start(_event=None):
if getattr(toplevel, "_md2gost_dnd_hooked", False):
return
if hook._install():
toplevel._md2gost_dnd_hooked = True
toplevel._md2gost_dnd_hook = hook
cls._hooks.append(hook)
widget.bind("<Map>", start, add="+")
widget.bind("<Destroy>", lambda e: hook._detach(), add="+")
try:
if widget.winfo_ismapped():
start()
else:
widget.after_idle(start)
except Exception:
widget.after(200, start)
return True
def _install(self) -> bool:
try:
api = _win32_drop_api()
except Exception:
return False
hwnd = _toplevel_hwnd(self.widget)
if not hwnd:
return False
WM_DROPFILES = 0x0233
GWLP_WNDPROC = -4
WS_EX_ACCEPTFILES = 0x00000010
GWL_EXSTYLE = -20
def wndproc(hw, msg, wp, lp):
if msg == WM_DROPFILES:
try:
hdrop = int(wp) if wp is not None else 0
if hdrop:
self._pending.extend(_query_drop_files(hdrop, api=api))
api.DragFinish(hdrop)
except Exception:
pass
return 0
if self._old_proc:
return api.CallWindowProc(self._old_proc, hw, msg, wp, lp)
return api.DefWindowProc(hw, msg, wp, lp)
self._wndproc = api.WNDPROC(wndproc)
try:
ex = api.GetWindowLong(hwnd, GWL_EXSTYLE) or 0
api.SetWindowLong(hwnd, GWL_EXSTYLE, int(ex) | WS_EX_ACCEPTFILES)
api.DragAcceptFiles(hwnd, True)
self._old_proc = api.GetWindowLongPtr(hwnd, GWLP_WNDPROC)
api.SetWindowLongPtr(hwnd, GWLP_WNDPROC, api.as_ptr(self._wndproc))
except Exception:
return False
self._hwnd = hwnd
self._poll()
return True
def _poll(self) -> None:
if not self._alive:
return
if self._pending:
files = self._pending[:]
self._pending.clear()
try:
self.callback(files)
except Exception:
pass
try:
self.widget.after(120, self._poll)
except Exception:
self._alive = False
def _detach(self) -> None:
self._alive = False
if not self._hwnd or self._old_proc is None:
return
try:
api = _win32_drop_api()
api.SetWindowLongPtr(self._hwnd, -4, self._old_proc)
except Exception:
pass
self._old_proc = None
class _Win32DropApi:
def __init__(self):
import ctypes
from ctypes import wintypes
self._ctypes = ctypes
is64 = ctypes.sizeof(ctypes.c_void_p) == 8
# wintypes.WPARAM/LPARAM historically were 32-bit; force pointer width.
WPARAM = ctypes.c_uint64 if is64 else ctypes.c_uint
LPARAM = ctypes.c_int64 if is64 else ctypes.c_long
HWND = ctypes.c_void_p
LRESULT = ctypes.c_int64 if is64 else ctypes.c_long
user32 = ctypes.WinDLL("user32", use_last_error=True)
shell32 = ctypes.WinDLL("shell32", use_last_error=True)
get_ptr = user32.GetWindowLongPtrW if is64 else user32.GetWindowLongW
set_ptr = user32.SetWindowLongPtrW if is64 else user32.SetWindowLongW
get_ptr.argtypes = [HWND, ctypes.c_int]
get_ptr.restype = ctypes.c_void_p
set_ptr.argtypes = [HWND, ctypes.c_int, ctypes.c_void_p]
set_ptr.restype = ctypes.c_void_p
# GWL_EXSTYLE is a 32-bit style mask; Get/SetWindowLongW is enough.
get_long = user32.GetWindowLongW
set_long = user32.SetWindowLongW
get_long.argtypes = [HWND, ctypes.c_int]
get_long.restype = ctypes.c_long
set_long.argtypes = [HWND, ctypes.c_int, ctypes.c_long]
set_long.restype = ctypes.c_long
call_proc = user32.CallWindowProcW
call_proc.argtypes = [ctypes.c_void_p, HWND, wintypes.UINT, WPARAM, LPARAM]
call_proc.restype = LRESULT
def_proc = user32.DefWindowProcW
def_proc.argtypes = [HWND, wintypes.UINT, WPARAM, LPARAM]
def_proc.restype = LRESULT
accept = shell32.DragAcceptFiles
accept.argtypes = [HWND, wintypes.BOOL]
accept.restype = None
finish = shell32.DragFinish
finish.argtypes = [ctypes.c_void_p]
finish.restype = None
query = shell32.DragQueryFileW
query.argtypes = [ctypes.c_void_p, wintypes.UINT, ctypes.c_wchar_p, wintypes.UINT]
query.restype = wintypes.UINT
self.shell32 = shell32
self.WNDPROC = ctypes.WINFUNCTYPE(LRESULT, HWND, wintypes.UINT, WPARAM, LPARAM)
self.GetWindowLongPtr = get_ptr
self.SetWindowLongPtr = set_ptr
self.GetWindowLong = get_long
self.SetWindowLong = set_long
self.CallWindowProc = call_proc
self.DefWindowProc = def_proc
self.DragAcceptFiles = accept
self.DragFinish = finish
self.DragQueryFileW = query
def as_ptr(self, wndproc):
return self._ctypes.cast(wndproc, self._ctypes.c_void_p).value
_WIN32_DROP_API = None
def _win32_drop_api() -> _Win32DropApi:
global _WIN32_DROP_API
if _WIN32_DROP_API is None:
_WIN32_DROP_API = _Win32DropApi()
return _WIN32_DROP_API
def _toplevel_hwnd(widget) -> int:
try:
import ctypes
hwnd = int(widget.winfo_id())
GA_ROOT = 2
root = ctypes.windll.user32.GetAncestor(hwnd, GA_ROOT)
if root:
return int(root)
parent = ctypes.windll.user32.GetParent(hwnd)
return int(parent or hwnd)
except Exception:
try:
return int(widget.winfo_id())
except Exception:
return 0
def _query_drop_files(hdrop: int, api: _Win32DropApi | None = None) -> list[str]:
import ctypes
query = (api or _win32_drop_api()).DragQueryFileW
count = query(hdrop, 0xFFFFFFFF, None, 0)
files: list[str] = []
for i in range(count):
length = query(hdrop, i, None, 0)
buf = ctypes.create_unicode_buffer(length + 1)
query(hdrop, i, buf, length + 1)
if buf.value:
files.append(buf.value)
return files
+148
View File
@@ -0,0 +1,148 @@
"""DOCX core properties (File → Info) for generated reports."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from getpass import getuser
from typing import Any
CORE_PROP_MAX = 255
AUTHOR_SOURCES = ("os", "student", "custom")
DEFAULT_AUTHOR_SOURCE = "os"
DEFAULT_DOC_COMMENTS = "Создано при помощи md2gost (ТЗ МИРЭА)"
def os_user() -> str:
try:
return (getuser() or "").strip()
except Exception:
return ""
def clip_core(value: str | None) -> str:
text = "" if value is None else str(value)
if len(text) > CORE_PROP_MAX:
return text[:CORE_PROP_MAX]
return text
def _normalize_source(value: str | None) -> str:
src = (value or DEFAULT_AUTHOR_SOURCE).strip().lower()
return src if src in AUTHOR_SOURCES else DEFAULT_AUTHOR_SOURCE
@dataclass
class DocumentMetadata:
author_source: str = DEFAULT_AUTHOR_SOURCE
author: str = ""
last_modified_by: str = ""
title: str = ""
subject: str = ""
keywords: str = ""
comments: str = DEFAULT_DOC_COMMENTS
category: str = ""
def to_dict(self) -> dict[str, str]:
return {
"author_source": _normalize_source(self.author_source),
"author": (self.author or "").strip(),
"last_modified_by": (self.last_modified_by or "").strip(),
"title": (self.title or "").strip(),
"subject": (self.subject or "").strip(),
"keywords": (self.keywords or "").strip(),
"comments": "" if self.comments is None else str(self.comments).strip(),
"category": (self.category or "").strip(),
}
@classmethod
def from_dict(cls, raw: object) -> DocumentMetadata:
if not isinstance(raw, dict):
return cls()
comments = raw.get("comments")
if comments is None:
comments_s = DEFAULT_DOC_COMMENTS
else:
comments_s = str(comments).strip()
return cls(
author_source=_normalize_source(str(raw.get("author_source") or "")),
author=str(raw.get("author") or "").strip(),
last_modified_by=str(raw.get("last_modified_by") or "").strip(),
title=str(raw.get("title") or "").strip(),
subject=str(raw.get("subject") or "").strip(),
keywords=str(raw.get("keywords") or "").strip(),
comments=comments_s,
category=str(raw.get("category") or "").strip(),
)
def _pick(override: str | None, fallback: str) -> str:
if override is not None:
return str(override)
return fallback
def resolve_document_metadata(
*,
stored: DocumentMetadata | None = None,
student: str | None = None,
author_source: str | None = None,
author: str | None = None,
last_modified_by: str | None = None,
title: str | None = None,
subject: str | None = None,
keywords: str | None = None,
comments: str | None = None,
category: str | None = None,
) -> DocumentMetadata:
"""Merge CLI/request overrides over the saved profile into concrete core properties."""
base = stored or DocumentMetadata()
src = _normalize_source(author_source or base.author_source)
if author is not None:
resolved_author = str(author).strip()
elif src == "custom":
resolved_author = (base.author or "").strip()
elif src == "student":
resolved_author = (student or "").strip()
else:
resolved_author = ""
if not resolved_author:
resolved_author = os_user()
last_mod = _pick(last_modified_by, base.last_modified_by).strip()
if not last_mod:
last_mod = resolved_author
return DocumentMetadata(
author_source=src,
author=resolved_author,
last_modified_by=last_mod,
title=_pick(title, base.title).strip(),
subject=_pick(subject, base.subject).strip(),
keywords=_pick(keywords, base.keywords).strip(),
comments=_pick(comments, base.comments).strip(),
category=_pick(category, base.category).strip(),
)
def apply_document_metadata(
document: Any,
meta: DocumentMetadata,
*,
now: datetime | None = None,
) -> None:
stamp = now or datetime.utcnow()
if stamp.tzinfo is not None:
stamp = stamp.replace(tzinfo=None)
cp = document.core_properties
cp.author = clip_core(meta.author)
cp.last_modified_by = clip_core(meta.last_modified_by or meta.author)
cp.title = clip_core(meta.title)
cp.subject = clip_core(meta.subject)
cp.keywords = clip_core(meta.keywords)
cp.comments = clip_core(meta.comments)
cp.category = clip_core(meta.category)
cp.created = stamp
cp.modified = stamp
cp.revision = 1
+25
View File
@@ -24,6 +24,7 @@ __all__ = [
"create_table_row", "create_table_row",
"create_table_cell", "create_table_cell",
"apply_cell_merge", "apply_cell_merge",
"set_table_box_borders",
] ]
@@ -117,3 +118,27 @@ def apply_cell_merge(cell: _Cell, v_merge: str | None = None, grid_span: int | N
tcPr.append(create_element("w:vMerge")) tcPr.append(create_element("w:vMerge"))
if grid_span and grid_span > 1: if grid_span and grid_span > 1:
tcPr.append(create_element("w:gridSpan", {"w:val": str(grid_span)})) tcPr.append(create_element("w:gridSpan", {"w:val": str(grid_span)}))
def set_table_box_borders(table: Table, *, sz: str = "4", color: str = "000000") -> None:
"""Outer frame only — no inside H/V lines (listings look like text in a box)."""
tbl = table._tbl
tblPr = tbl.tblPr
if tblPr is None:
tblPr = create_element("w:tblPr")
tbl.insert(0, tblPr)
for el in list(tblPr.findall(qn("w:tblBorders"))):
tblPr.remove(el)
solid = {"w:val": "single", "w:sz": sz, "w:space": "0", "w:color": color}
none = {"w:val": "nil"}
borders = create_element("w:tblBorders")
for edge, attrs in (
("w:top", solid),
("w:left", solid),
("w:bottom", solid),
("w:right", solid),
("w:insideH", none),
("w:insideV", none),
):
borders.append(create_element(edge, attrs))
tblPr.append(borders)
+55
View File
@@ -0,0 +1,55 @@
"""Attach SVG as Word svgBlip alongside a PNG InlineShape (Office 2016+)."""
from __future__ import annotations
from pathlib import Path
from docx.opc.constants import RELATIONSHIP_TYPE as RT
from docx.opc.part import Part
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from lxml import etree
SVG_CONTENT_TYPE = "image/svg+xml"
SVG_BLIP_URI = "{96DAC541-7B7A-43C6-8E14-B03AE682B59D}"
ASVG_NS = "http://schemas.microsoft.com/office/drawing/2016/SVG/main"
ASVG_SVG_BLIP = f"{{{ASVG_NS}}}svgBlip"
def attach_svg_blip(run, inline_shape, svg_path: str | Path) -> bool:
"""
After run.add_picture(png), add an SVG part and asvg:svgBlip on the PNG blip.
Word 2016+ uses the vector; older apps keep the PNG.
Returns True if the SVG was attached.
"""
path = Path(svg_path)
if not path.is_file() or path.stat().st_size <= 0:
return False
data = path.read_bytes()
head = data.lstrip()[:200].lower()
if not (head.startswith(b"<svg") or head.startswith(b"<?xml") or b"<svg" in head):
return False
part = run.part
package = part.package
partname = package.next_partname("/word/media/image%d.svg")
svg_part = Part(partname, SVG_CONTENT_TYPE, data, package)
r_id = part.relate_to(svg_part, RT.IMAGE)
blip = inline_shape._inline.graphic.graphicData.pic.blipFill.blip
ext_lst = blip.find(qn("a:extLst"))
if ext_lst is None:
ext_lst = OxmlElement("a:extLst")
blip.append(ext_lst)
# Drop any previous svgBlip extension with the same URI.
for ext in list(ext_lst.findall(qn("a:ext"))):
if ext.get("uri") == SVG_BLIP_URI:
ext_lst.remove(ext)
ext = OxmlElement("a:ext")
ext.set("uri", SVG_BLIP_URI)
svg_blip = etree.SubElement(ext, ASVG_SVG_BLIP)
svg_blip.set(qn("r:embed"), r_id)
ext_lst.append(ext)
return True
+20 -5
View File
@@ -4,23 +4,38 @@ from re import Match, compile as re_compile
_LISTING_FLAG_RE = re_compile(r"(?i)(?:^|\s)\+?listing\b") _LISTING_FLAG_RE = re_compile(r"(?i)(?:^|\s)\+?listing\b")
_LANDSCAPE_FLAG_RE = re_compile(r"(?i)(?:^|\s)\+?landscape\b")
def strip_caption_flags(raw: str) -> tuple[str | None, bool, bool]:
"""Return (clean_text, with_listing, landscape) from caption / image-title tail."""
text = (raw or "").strip()
with_listing = bool(_LISTING_FLAG_RE.search(text))
landscape = bool(_LANDSCAPE_FLAG_RE.search(text))
if with_listing:
text = _LISTING_FLAG_RE.sub(" ", text)
if landscape:
text = _LANDSCAPE_FLAG_RE.sub(" ", text)
text = " ".join(text.split()).strip()
return (text or None), with_listing, landscape
class Caption(BlockElement): class Caption(BlockElement):
"""Represents caption element """Represents caption element
Syntax: %label Caption text [+listing] Syntax: %label Caption text [+listing] [+landscape]
""" """
# Interrupt an open paragraph so «текст:\\n%id …» still becomes a Caption
# (otherwise marko treats the % line as paragraph continuation / plain text).
breaks_paragraph = True
priority = 6
pattern = r"\%(\w+)( (.+))?" pattern = r"\%(\w+)( (.+))?"
def __init__(self, match: Match[str]): def __init__(self, match: Match[str]):
self.unique_name = match.group(1) self.unique_name = match.group(1)
raw = (match.group(3) or "").strip() raw = (match.group(3) or "").strip()
self.with_listing = bool(_LISTING_FLAG_RE.search(raw)) self.text, self.with_listing, self.landscape = strip_caption_flags(raw)
if self.with_listing:
raw = _LISTING_FLAG_RE.sub(" ", raw).strip()
self.text = raw or None
@classmethod @classmethod
def match(cls, source: Source) -> Match[str] | None: def match(cls, source: Source) -> Match[str] | None:
+1 -1
View File
@@ -20,7 +20,7 @@ class Heading(BlockElement):
def __init__(self, match: Match[str]) -> None: def __init__(self, match: Match[str]) -> None:
self.level = len(match.group(1)) self.level = len(match.group(1))
inline_body = match.group(2).strip() inline_body = match.group(2).strip()
self.numbered = not (inline_body[0] == "*") self.numbered = not inline_body.startswith("*")
if not self.numbered: if not self.numbered:
inline_body = inline_body[1:] inline_body = inline_body[1:]
self.inline_body = inline_body self.inline_body = inline_body
+7 -1
View File
@@ -2,6 +2,8 @@ import re
from marko.inline import Image as Image_ from marko.inline import Image as Image_
from .caption import strip_caption_flags
class Image(Image_): class Image(Image_):
override = True override = True
@@ -9,7 +11,11 @@ class Image(Image_):
super().__init__(match) super().__init__(match)
self.unique_name = None self.unique_name = None
self.with_listing = False
self.landscape = False
if self.title and (m := re.match(r"\%(\w+)( (.+))?", self.title)): if self.title and (m := re.match(r"\%(\w+)( (.+))?", self.title)):
self.unique_name = m.group(1) self.unique_name = m.group(1)
self.title = (m.group(3) or "").strip() or None self.title, self.with_listing, self.landscape = strip_caption_flags(
(m.group(3) or "").strip()
)
+1888
View File
File diff suppressed because it is too large Load Diff
+400
View File
@@ -0,0 +1,400 @@
"""Usage text and AI prompt files for the GUI pages."""
from __future__ import annotations
import sys
from pathlib import Path
from . import package_dir
USAGE_HELP = """md2gost — Markdown → DOCX (ТЗ МИРЭА / ГОСТ 7.32)
КАК ПОЛЬЗОВАТЬСЯ ОКНОМ
1. Перетащите .md в верхнюю область (или кликните по ней).
2. Выберите тип документа и параметры в блоке «Основные».
Шаблон / титул / задание — Настройки → Файлы.
ФИО и группа студента — Настройки → Студент… (спрашивают при первом запуске).
Свойства DOCX (автор, название, примечание) — Настройки → Метаданные….
PlantUML / Mermaid / Kroki — Настройки → Диаграммы.
Свои UML-схемы — меню «Шаблоны UML».
3. Нажмите «Конвертировать». Документ сохранится рядом с исходником (или по пути «Выходной DOCX»).
Если файл уже есть и вы откажетесь перезаписывать — сохранится как имя_гггг-мм-дд-ЧЧ-ММ.docx.
Дебаг (меню сверху) — следующая сборка с отладочными данными в документе.
Типы: practice (по умолчанию) / coursework / vkr — ГОСТ МИРЭА; PIS_custom — отчёт по практикам ПИС; APID_coursework — курсовая АПИД.
Полезные галочки
• «—» → «-» — заменить типографское тире на дефис (по умолчанию **выкл.**; методичка требует «—»).
• --- → разрыв страницы — по умолчанию выкл. (строка --- игнорируется). Вкл. — page break в Word.
• Смещение страниц — начальный номер PAGE в теле (пусто = сквозной; 1 = тело с 1).
• Проверить по ТЗ — замечания по структуре, «рис.», источникам.
• Проверить вёрстку в Word — полупустые страницы (эвристика, возможны ложные срабатывания; нужны Word + pywin32).
• Титул / задание — отдельные DOCX, вставляются перед телом отчёта.
Автотитул — галочка «Автотитул» / CLI `--auto-title` (по умолчанию **выкл.**):
info_conv.yaml рядом с .md + опционально блок ```title (number, year…);
ФИО/группа — Настройки → Студент… или --student / --group.
Явный путь к титулу отключает генератор. Подробно: docs/title.md.
• Свойства файла Word — Настройки → Метаданные… / --doc-author-from, --doc-author, --doc-title, --doc-comments….
По умолчанию автор = имя Windows, примечание «Создано при помощи md2gost (ТЗ МИРЭА)».
CLI (тот же движок)
python -m md2gost report.md -o report.docx --type coursework --check
python -m md2gost report.md --auto-title --student "Иванов И. И." --group ИНБО-31-23
python -m md2gost report.md --doc-author-from student --doc-comments ""
python -m md2gost report.docx -o report.md
python -m word2md report.docx
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
Импорт Word → Markdown
Перетащите .docx или кнопка «Импорт DOCX→MD». Подписи, таблицы, спецразделы
переводятся в диалект md2gost. UML/Mermaid из картинок не восстанавливаются.
Подробнее: docs/word2md.md.
Стили JSON (опционально)
Оверлей поверх пресета типа документа (--type). Файл md2gost.styles.json рядом с .md
или --styles / Настройки → Файлы → «Стили JSON». Меняет поля страницы и параметры
стилей абзацев (Normal, Heading 1–3, подписи…). В шаблон DOCX стили руками добавлять не нужно.
Подробнее: docs/styles.md.
СИНТАКСИС MARKDOWN
Спецразделы (без номера, ПРОПИСНЫЕ, звёздочка):
# *СОДЕРЖАНИЕ
[TOC]
# *ВВЕДЕНИЕ
# *ЗАКЛЮЧЕНИЕ
# *СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ
# *ПРИЛОЖЕНИЯ
(перечень «Приложение А — …» собирается сам из ## Приложение А Название;
можно написать вручную — тогда автосписок не дублируется)
## Приложение А Название
Нумерованные разделы: # 1 Название ## 1.1 Подраздел
Точку в конце заголовка не ставить.
СОДЕРЖАНИЕ и СПИСОК — по центру; Введение / Заключение / ПРИЛОЖЕНИЯ — слева.
Рисунок
Текст со ссылкой на @Рисунок:arch.
![описание](images/arch.png "%arch Название рисунка")
В тексте пишите «Рисунок», не «рис.»
Таблица
%tbl1 Название
| A | B |
|---|---|
| 1 | 2 |
Ссылка: @Таблица:tbl1
Склеивание: ^ — ячейка сверху (rowspan), > — ячейка слева (colspan).
Не ставить ^/> в шапке; > — не в первом столбце.
Листинг
%code1 Название
```python
print("ok")
```
Диаграмма (PlantUML / Mermaid / схемы) → рисунок
%usecase1 Диаграмма прецедентов +listing
```uml
@startuml
actor User
User --> (Login)
@enduml
```
Широкая схема/таблица на альбомной странице:
%arch1 Архитектура +landscape
```uml-c4
…
```
Mermaid (локально: браузер / QuickJS; иначе Kroki):
%seq Последовательность +listing
```mermaid
sequenceDiagram
Alice->>Bob: hello
```
IDEF0 (контекст A-0, рамка NIST):
%a0 Контекст +landscape
```idef0
title Распорядиться товаром
node A-0
[A0] Распорядиться товаром
<- Спрос
^ Нормативная документация
-> Товар
v Персонал
```
DFD (pbauermeister/dfd → Graphviz вшитый/PATH / Kroki):
%dfd0 Контекстная DFD
```dfd
style context
entity Client Клиент
process System Система учёта заявок
Client --> System заявка
System --> Client статус
```
+listing — ещё и листинг с исходником.
+landscape — отдельная альбомная страница (A4, повёрт на 90°) вокруг рисунка/таблицы, затем снова книжная.
Языки: uml, plantuml, mermaid / mmd, idef0, dfd / data-flow-diagram, или uml-<id> / <id> из файла схем (c4, usecase, archimate, …).
Формат в Word: PNG по умолчанию (PlantUML, локальный Mermaid, IDEF0 и DFD ~2× для чёткости); --diagram-format svg — вектор + PNG-запасной (Word 2016+).
Подробнее — Справка → Схемы и раздел ниже в инструкции.
Формула (номер только если есть ссылка)
%eq1
$$ E = mc^2 $$
См. @Формула:eq1
Источники
В тексте: [1] (в DOCX — ссылка на пункт списка)
В списке: [1]: Иванов И. И. Название. — М.: Наука, 2023. — 120 с.
Разрыв страницы
По умолчанию --- игнорируется.
Галочка «--- → разрыв страницы» или --hr-pagebreak: пустая строка, ---, пустая строка.
Нумерация заголовков
manual — цифры уже в md (# 1 …); auto — нумерует Word.
Содержание
native — поле Word (обновить при открытии); manual — собирает md2gost.
Продолжение таблиц / листингов
word — по умолчанию: после сборки Word COM режет по реальной пагинации и
вставляет «Продолжение…» (нужны Windows + Word + pywin32). Также уводит
таблицу целиком, если внизу страницы зависли только название или
название + шапка граф (методичка: рис. Г.3).
off — одна таблица, Word сам переносит строки; без авто«Продолжение».
Защита от сирот: keep_with_next у подписи/шапки и оценка
«подпись+шапка+1 строка» при рендере (без COM слабее, чем word).
legacy / caption — режем по оценке высоты в md2gost (может не совпасть с Word).
Промпт для ИИ — Справка → Промпт для ИИ: выберите промпт, при необходимости
включите схемы (C4, usecase, …) кнопками — макросы допишутся в конец — скопируйте
в ChatGPT / Cursor / Copilot, затем дайте тему и черновик.
Документация — Справка → Документация: встроенный просмотр docs/*.md
(вшито в exe; внешняя папка docs/ не обязательна).
"""
SCHEMES_HELP = """СХЕМЫ ДИАГРАММ (PlantUML)
Зачем
В markdown пишете только «тело» диаграммы. Обёртка (@startuml, !include, тема)
берётся из схемы в файле md2gost.schemes.json.
Первый запуск
Рядом с программой (рядом с md2gost.exe или в текущей папке при python -m)
создаётся md2gost.schemes.json из встроенного шаблона.
При каждом запуске схемы с author=md2gost синхронизируются с шаблоном приложения
(добавление новых id, обновление, удаление исчезнувших вроде bpmn).
Схемы с пустым или другим author не трогаются.
Если правите встроенную схему в GUI и сохраняете — author сбрасывается,
чтобы следующая миграция не затёрла ваши правки.
Оградка в markdown
```uml — обычный PlantUML (или ```plantuml)
```uml-c4 — схема с id «c4» (то же, что ```c4)
```uml-usecase — схема «usecase»
```uml-archimate — ArchiMate 3.2 (макросы Business_*, Rel_Serving, …)
```mermaid / ```mmd — Mermaid локально (браузер / Chromium / QuickJS) или через Kroki
```idef0 — IDEF0 (FIPS 183): боксы, стрелки ICOM, рамка NIST; локальный Pillow
```dfd / ```data-flow-diagram — DFD (pbauermeister/dfd): process/entity/store, --> ;
Graphviz (вшитый / «Скачать Graphviz» / PATH) или Kroki
Встроенные пресеты PlantUML: c4, c4context, c4component, usecase.
Пример
%arch Архитектура +listing
```uml-c4
Person(user, "Студент")
System(app, "Портал")
Rel(user, app, "логин")
```
Поля схемы в JSON
title — подпись в GUI
version — версия схемы
author — автор
docs — шпаргалка синтаксиса (чтобы вспомнить макросы)
ai-prompt — заготовка промпта для ИИ / будущего MCP
includes — список файлов или http(s):// URL на .puml
prefix — текст перед телом (часто @startuml)
postfix — текст после тела (часто @enduml)
theme — опционально !theme …
Свои схемы
1. Меню «Шаблоны UML» в GUI — добавьте / отредактируйте и сохраните.
2. Или откройте md2gost.schemes.json в редакторе («Открыть JSON» / «Открыть файл схем»).
3. CLI: --schemes путь.json; также подхватывается md2gost.schemes.json рядом с .md.
Кэш includes из интернета
URL из includes (и !include https://… внутри .puml) при первом рендере
скачиваются. Индекс — md2gost.include-cache.json (только пары URL → файл).
Файлы лежат в папке include-cache/. Уже скачанные файлы не перезаписываются.
Повторный рендер без сети берёт путь из индекса.
«Сбросить кэш includes» в Настройки → Диаграммы или в «Шаблоны UML» удаляет индекс
и только файлы, перечисленные в нём.
Чего нет
Балансировка родитель/потомок DFD между уровнями не проверяется автоматически.
Рендер
UML: Java + plantuml.jar (локально) → иначе локальный Kroki → иначе kroki.io.
Mermaid: системный Chrome/Edge → Playwright Chromium (кнопка / --install-chromium) →
QuickJS (mermaidx, офлайн) → локальный Kroki → kroki.io.
IDEF0: Pillow, без сети и Java.
DFD: data-flow-diagram → Graphviz (dot/neato) или Kroki graphviz.
Нужны: Graphviz (вшитый / «Скачать Graphviz» / PATH) либо Kroki;
пакет data-flow-diagram ставится с md2gost.
Формат: PNG по умолчанию (PlantUML / локальный Mermaid ~scale 2 для чёткости, размер на странице как при 1; --diagram-scale); --diagram-format svg — PNG + SVG (svgBlip в Word 2016+).
+landscape у %подписи — альбомная страница под широкий рисунок/таблицу.
"""
PROMPT_FILES = (
("generate-md.md", "Markdown для md2gost"),
("generate-mirea-report.md", "МИРЭА / ГОСТ (курсовая, практика, ВКР)"),
("generate-pis-custom-report.md", "ПИС — отчёт по практическим работам"),
("emulate-student.md", "Стиль: человечный текст, не отчёт ИИ"),
)
def prompt_search_dirs() -> list[Path]:
dirs: list[Path] = []
here = Path(package_dir())
dirs.append(here / "prompts")
dirs.append(here.parent / "prompts")
if getattr(sys, "frozen", False):
mei = getattr(sys, "_MEIPASS", None)
if mei:
dirs.append(Path(mei) / "prompts")
dirs.append(Path(sys.executable).resolve().parent / "prompts")
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 load_prompt_catalog() -> list[tuple[str, str, str]]:
"""Return list of (filename, title, text). Missing files are skipped."""
catalog: list[tuple[str, str, str]] = []
dirs = prompt_search_dirs()
for name, title in PROMPT_FILES:
text = None
for folder in dirs:
candidate = folder / name
if candidate.is_file():
text = candidate.read_text(encoding="utf-8")
break
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
+2159
View File
File diff suppressed because it is too large Load Diff
-3
View File
@@ -102,9 +102,6 @@ def assign_numbers(renderables: list[Renderable],
r.set_listing_number(lnum) r.set_listing_number(lnum)
if label: if label:
registry.put("Листинг", label, lnum) registry.put("Листинг", label, lnum)
# Images nested in paragraphs
if isinstance(r, Paragraph):
pass
# Images attached to paragraphs # Images attached to paragraphs
if isinstance(r, Paragraph): if isinstance(r, Paragraph):
+13 -4
View File
@@ -8,15 +8,24 @@ from lxml.etree import _Element
from . import package_dir from . import package_dir
_MML2OMML_TRANSFORM: etree.XSLT | None = None
def _mml2omml_transform() -> etree.XSLT:
"""The stylesheet is stateless and reusable, so parse and compile it once."""
global _MML2OMML_TRANSFORM
if _MML2OMML_TRANSFORM is None:
_MML2OMML_TRANSFORM = etree.XSLT(etree.parse(
os.path.join(package_dir(), "mml2omml")
))
return _MML2OMML_TRANSFORM
def latex_to_omml(latex_equation: str) -> _Element: def latex_to_omml(latex_equation: str) -> _Element:
try: try:
mathml = latex2mathml.converter.convert(latex_equation) mathml = latex2mathml.converter.convert(latex_equation)
tree = etree.fromstring(mathml) tree = etree.fromstring(mathml)
xslt = etree.parse( transform = _mml2omml_transform()
os.path.join(package_dir(), "mml2omml")
)
transform = etree.XSLT(xslt)
new_dom = transform(tree) new_dom = transform(tree)
word_math = new_dom.getroot() word_math = new_dom.getroot()
except Exception: except Exception:
+10
View File
@@ -51,3 +51,13 @@ class LayoutTracker:
def new_page(self): def new_page(self):
self._state.new_page() self._state.new_page()
def set_page_size(self, max_height: Length, max_width: Length) -> None:
"""Switch usable area (e.g. portrait ↔ landscape) and start a fresh page."""
self._state.max_height = max_height
self._state.max_width = max_width
# Align to a page boundary under the *new* geometry.
rem = self._state.remaining_page_height
if rem != max_height:
self._state.add_height(rem)
self._is_new_page = True
+600
View File
@@ -0,0 +1,600 @@
"""Local Mermaid rendering: system browser → Playwright Chromium → QuickJS (mermaidx)."""
from __future__ import annotations
import logging
import os
import subprocess
import threading
from pathlib import Path
from typing import Literal
from . import package_dir
_log = logging.getLogger(__name__)
# Pinned assets next to PlantUML schemes.
MERMAID_JS_NAME = "mermaid.min.js"
MERMAID_PAGE_NAME = "mermaid_page.html"
CACHE_KEY_PREFIX = "mmd-local-v4\n"
# QuickJS/resvg cannot paint HTML foreignObject — use SVG <text> there only.
# Browser keeps htmlLabels:true (correct box sizing for Cyrillic) + PNG via screenshot.
_MERMAID_CONFIG_QUICKJS = {
"flowchart": {"htmlLabels": False},
"themeVariables": {
"fontFamily": '"Segoe UI", "DejaVu Sans", Arial, sans-serif',
},
}
_MERMAID_INIT_QUICKJS = (
"%%{init: {"
"'flowchart': {'htmlLabels': false}, "
"'themeVariables': {'fontFamily': '\"Segoe UI\", \"DejaVu Sans\", Arial, sans-serif'}"
"}}%%\n"
)
_MERMAID_INIT_BROWSER = (
"%%{init: {"
"'flowchart': {"
"'htmlLabels': true, "
"'wrappingWidth': 320, "
"'subGraphTitleMargin': {'top': 12, 'bottom': 16}, "
"'padding': 20"
"}, "
"'themeVariables': {'fontFamily': '\"Segoe UI\", \"DejaVu Sans\", Arial, sans-serif'}"
"}}%%\n"
)
def prepare_mermaid_source(source: str, *, html_labels: bool = True) -> str:
"""
Prepend %%{init}%%.
html_labels=True — browser (correct layout for Cyrillic, PNG via screenshot).
html_labels=False — QuickJS/resvg (SVG <text> so glyphs are not dropped).
"""
text = source.lstrip("\ufeff").strip()
if text.startswith("%%{init"):
return text
return (_MERMAID_INIT_BROWSER if html_labels else _MERMAID_INIT_QUICKJS) + text
# Prefer installed browsers; bundled Chromium is optional (button / --install-chromium).
_SYSTEM_CHANNELS = ("msedge", "chrome", "chromium")
_lock = threading.RLock()
_session: "_BrowserSession | None" = None
_UNSET = object()
_cached_system_channel: str | None | object = _UNSET
def diagrams_dir() -> Path:
return Path(package_dir()) / "diagrams"
def _windows_browser_exe(channel: str) -> Path | None:
"""Best-effort path probe so GUI status does not launch browsers."""
local = Path(os.environ.get("LOCALAPPDATA") or "")
pf = Path(os.environ.get("PROGRAMFILES") or r"C:\Program Files")
pf86 = Path(os.environ.get("PROGRAMFILES(X86)") or r"C:\Program Files (x86)")
candidates: list[Path] = []
if channel == "msedge":
candidates = [
pf / "Microsoft" / "Edge" / "Application" / "msedge.exe",
pf86 / "Microsoft" / "Edge" / "Application" / "msedge.exe",
]
elif channel == "chrome":
candidates = [
pf / "Google" / "Chrome" / "Application" / "chrome.exe",
pf86 / "Google" / "Chrome" / "Application" / "chrome.exe",
local / "Google" / "Chrome" / "Application" / "chrome.exe",
]
elif channel == "chromium":
candidates = [
pf / "Chromium" / "Application" / "chrome.exe",
local / "Chromium" / "Application" / "chrome.exe",
]
for path in candidates:
if path.is_file():
return path
return None
def mermaid_assets_dir() -> Path:
return diagrams_dir()
def mermaid_page_path() -> Path:
return mermaid_assets_dir() / MERMAID_PAGE_NAME
def mermaid_js_path() -> Path:
return mermaid_assets_dir() / MERMAID_JS_NAME
def user_data_root() -> Path:
if os.name == "nt":
return Path(os.environ.get("LOCALAPPDATA") or Path.home()) / "md2gost"
return Path.home() / ".md2gost"
def playwright_browsers_dir() -> Path:
env = os.environ.get("PLAYWRIGHT_BROWSERS_PATH")
if env and env not in ("0", "1"):
return Path(env)
return user_data_root() / "ms-playwright"
def ensure_playwright_browsers_env() -> Path:
"""Point Playwright at %LOCALAPPDATA%/md2gost/ms-playwright (or ~/.md2gost)."""
root = playwright_browsers_dir()
root.mkdir(parents=True, exist_ok=True)
os.environ["PLAYWRIGHT_BROWSERS_PATH"] = str(root)
return root
def mermaidx_available() -> bool:
try:
import mermaidx # noqa: F401
return True
except ImportError:
return False
def playwright_available() -> bool:
try:
from playwright.sync_api import sync_playwright # noqa: F401
return True
except ImportError:
return False
def _playwright_driver() -> tuple[str, str] | None:
try:
from playwright._impl._driver import compute_driver_executable
node, cli = compute_driver_executable()
return str(node), str(cli)
except Exception as exc:
_log.debug("Playwright driver: %s", exc)
return None
def chromium_installed() -> bool:
"""True if Playwright Chromium binary exists under PLAYWRIGHT_BROWSERS_PATH."""
if not playwright_available():
return False
ensure_playwright_browsers_env()
try:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
exe = Path(p.chromium.executable_path)
return exe.is_file()
except Exception:
return False
def detect_system_browser_channel(*, probe_launch: bool = False) -> str | None:
"""
Return first usable Playwright channel (msedge / chrome / chromium).
By default only checks known install paths (fast, for GUI status).
Set probe_launch=True to actually launch (used once before first render).
"""
global _cached_system_channel
if _cached_system_channel is not _UNSET and not probe_launch:
return _cached_system_channel # type: ignore[return-value]
if not playwright_available():
_cached_system_channel = None
return None
# Path probe first (Windows); on other OS fall through to launch probe.
if os.name == "nt":
for channel in _SYSTEM_CHANNELS:
if _windows_browser_exe(channel) is not None:
_cached_system_channel = channel
return channel
if not probe_launch:
_cached_system_channel = None
return None
if not probe_launch and os.name == "nt":
return None
try:
from playwright.sync_api import sync_playwright
except ImportError:
_cached_system_channel = None
return None
ensure_playwright_browsers_env()
for channel in _SYSTEM_CHANNELS:
try:
with sync_playwright() as p:
browser = p.chromium.launch(channel=channel, headless=True)
browser.close()
_cached_system_channel = channel
return channel
except Exception as exc:
_log.debug("channel %s unavailable: %s", channel, exc)
_cached_system_channel = None
return None
def install_playwright_chromium() -> bool:
"""Download Playwright Chromium into user cache. Safe for frozen exe."""
if not playwright_available():
_log.warning("playwright не установлен — нельзя скачать Chromium")
return False
driver = _playwright_driver()
if not driver:
_log.warning("Playwright driver не найден")
return False
node, cli = driver
root = ensure_playwright_browsers_env()
env = os.environ.copy()
env["PLAYWRIGHT_BROWSERS_PATH"] = str(root)
cmd = [node, cli, "install", "chromium"]
_log.info("Установка Playwright Chromium → %s", root)
try:
proc = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=600,
check=False,
env=env,
)
except (OSError, subprocess.TimeoutExpired) as exc:
_log.warning("install chromium failed: %s", exc)
return False
if proc.returncode != 0:
_log.warning(
"playwright install chromium → %s\n%s\n%s",
proc.returncode,
proc.stdout,
proc.stderr,
)
return False
ok = chromium_installed()
if not ok:
_log.warning("Chromium скачан, но executable_path не найден")
return ok
def mermaid_engine_status() -> str:
"""Human-readable Mermaid status for the GUI."""
lines: list[str] = []
if not playwright_available():
lines.append("Mermaid browser: playwright не установлен")
else:
ch = detect_system_browser_channel()
if ch:
lines.append(f"Mermaid browser: системный ({ch})")
else:
lines.append("Mermaid browser: системный Chrome/Edge не найден")
if chromium_installed():
lines.append(f"Mermaid Chromium: есть ({playwright_browsers_dir()})")
else:
lines.append(
"Mermaid Chromium: нет. Кнопка «Скачать headless Chromium» "
"или md2gost --install-chromium"
)
if mermaidx_available():
lines.append("Mermaid QuickJS (mermaidx): готов (офлайн без браузера)")
else:
lines.append("Mermaid QuickJS: mermaidx не установлен")
page = mermaid_page_path()
js = mermaid_js_path()
if page.is_file() and js.is_file():
lines.append(f"Mermaid assets: {js.name} + {page.name}")
else:
lines.append("Mermaid assets: нет mermaid.min.js / mermaid_page.html")
return "\n".join(lines)
def svg_to_png_bytes(svg: str, *, scale: float = 1.0) -> bytes | None:
try:
from mermaidx import svg_to_png
except ImportError:
return None
try:
if scale and scale > 1:
return _svg_to_png_scaled(svg, float(scale))
return svg_to_png(svg)
except Exception as exc:
_log.warning("svg→png failed: %s", exc)
return None
def _svg_to_png_scaled(svg: str, scale: float) -> bytes:
from mermaidx import svg_to_png
if scale <= 1:
return svg_to_png(svg)
# Parse width="N" or width="Npx" from root svg; multiply for sharper raster.
import re
m = re.search(r"<svg\b[^>]*\bwidth=['\"]([\d.]+)(px)?['\"]", svg, re.I)
if m:
w = float(m.group(1)) * scale
return svg_to_png(svg, width=w)
# Fallback: resvg default size, then Pillow upsample (last resort)
png = svg_to_png(svg)
try:
from io import BytesIO
from PIL import Image
im = Image.open(BytesIO(png))
im = im.resize(
(max(1, int(im.width * scale)), max(1, int(im.height * scale))),
Image.Resampling.LANCZOS,
)
buf = BytesIO()
im.save(buf, format="PNG")
return buf.getvalue()
except Exception:
return png
def _render_quickjs(source: str, *, scale: float = 1.0) -> tuple[str, bytes] | None:
if not mermaidx_available():
return None
try:
from mermaidx import render
except ImportError:
return None
try:
prepared = prepare_mermaid_source(source, html_labels=False)
diagram = render(prepared, backend="quickjs", config=_MERMAID_CONFIG_QUICKJS)
svg = diagram.svg()
if not isinstance(svg, str) or "<svg" not in svg.lower():
return None
scale_kw = {"scale": float(scale)} if scale and scale > 1 else {}
png = diagram.png(**scale_kw)
if not isinstance(png, (bytes, bytearray)) or not png.startswith(b"\x89PNG"):
png_b = svg_to_png_bytes(svg, scale=scale)
if not png_b:
return None
png = png_b
return svg, bytes(png)
except Exception as exc:
_log.warning("mermaidx QuickJS failed: %s", exc)
return None
class _BrowserSession:
"""One Playwright browser for the whole conversion (system channel or bundled Chromium)."""
def __init__(self, *, channel: str | None, use_bundled: bool) -> None:
from playwright.sync_api import sync_playwright
ensure_playwright_browsers_env()
self._pw_cm = sync_playwright()
self._pw = self._pw_cm.__enter__()
kwargs: dict = {"headless": True}
if channel:
kwargs["channel"] = channel
elif not use_bundled:
raise RuntimeError("no browser channel")
self._browser = self._pw.chromium.launch(**kwargs)
# device_scale_factor set per render via new context when needed; default 1.
self._context = self._browser.new_context(device_scale_factor=1)
self._page = self._context.new_page()
page_file = mermaid_page_path()
js_file = mermaid_js_path()
if not page_file.is_file() or not js_file.is_file():
raise FileNotFoundError(f"Mermaid assets missing under {mermaid_assets_dir()}")
self._page.goto(page_file.resolve().as_uri(), wait_until="load")
self._page.wait_for_function("typeof window.__md2gostRender === 'function'")
self._scale = 1.0
def _ensure_scale(self, scale: float) -> None:
"""Recreate context if DPR must change for sharper screenshots."""
want = max(1.0, float(scale) if scale else 1.0)
if abs(want - self._scale) < 0.01:
return
self._context.close()
self._context = self._browser.new_context(device_scale_factor=want)
self._page = self._context.new_page()
self._page.goto(mermaid_page_path().resolve().as_uri(), wait_until="load")
self._page.wait_for_function("typeof window.__md2gostRender === 'function'")
self._scale = want
def render_png(self, source: str, *, scale: float = 1.0) -> tuple[str, bytes]:
"""
Browser: htmlLabels + screenshot at viewBox size (layout + Cyrillic + hi-res).
"""
self._ensure_scale(scale)
prepared = prepare_mermaid_source(source, html_labels=True)
svg = self._page.evaluate(
"(code) => window.__md2gostRender(code)",
prepared,
)
if not isinstance(svg, str) or "<svg" not in svg.lower():
raise RuntimeError("browser mermaid.render returned no SVG")
# Mermaid sets width="100%" — force viewBox pixel size before capture.
dims = self._page.evaluate(
"""() => {
const el = document.querySelector('#out svg');
if (!el) return null;
let w = 0, h = 0;
try {
const vb = el.viewBox && el.viewBox.baseVal;
if (vb && vb.width > 0 && vb.height > 0) {
w = vb.width;
h = vb.height;
}
} catch (e) {}
if (!(w > 0 && h > 0)) {
try {
const b = el.getBBox();
w = b.width;
h = b.height;
} catch (e) {}
}
if (!(w > 0 && h > 0)) return null;
el.setAttribute('width', String(w));
el.setAttribute('height', String(h));
el.style.width = w + 'px';
el.style.height = h + 'px';
el.style.maxWidth = 'none';
el.style.maxHeight = 'none';
return { w: Math.ceil(w), h: Math.ceil(h) };
}"""
)
if isinstance(dims, dict):
vw = max(1280, int(dims.get("w") or 0) + 64)
vh = max(720, int(dims.get("h") or 0) + 64)
self._page.set_viewport_size({"width": vw, "height": vh})
# Prefer screenshot: FO/HTML labels render correctly in a real browser.
png: bytes | None = None
loc = self._page.locator("#out svg").first
loc.wait_for(state="visible", timeout=15000)
shot = loc.screenshot(type="png", omit_background=False)
if isinstance(shot, (bytes, bytearray)) and shot.startswith(b"\x89PNG"):
png = bytes(shot)
if png is None and "foreignObject" not in svg:
png = svg_to_png_bytes(svg, scale=scale)
if not png:
raise RuntimeError("no PNG from browser path")
return svg, png
def close(self) -> None:
try:
self._browser.close()
except Exception:
pass
try:
self._pw_cm.__exit__(None, None, None)
except Exception:
pass
def reset_mermaid_session() -> None:
global _session
with _lock:
if _session is not None:
try:
_session.close()
except Exception:
pass
_session = None
def _get_session(*, prefer_system: bool = True) -> _BrowserSession | None:
global _session
with _lock:
if _session is not None:
return _session
if not playwright_available():
return None
page = mermaid_page_path()
if not page.is_file() or not mermaid_js_path().is_file():
return None
# 1) system browser
if prefer_system:
ch = detect_system_browser_channel(probe_launch=True)
if ch:
try:
_session = _BrowserSession(channel=ch, use_bundled=False)
_log.info("Mermaid: системный браузер (%s)", ch)
return _session
except Exception as exc:
_log.warning("Mermaid system browser failed: %s", exc)
_session = None
# Retry other channels once
for alt in _SYSTEM_CHANNELS:
if alt == ch:
continue
try:
_session = _BrowserSession(channel=alt, use_bundled=False)
_log.info("Mermaid: системный браузер (%s)", alt)
return _session
except Exception:
_session = None
# 2) Playwright Chromium
if chromium_installed():
try:
_session = _BrowserSession(channel=None, use_bundled=True)
_log.info("Mermaid: Playwright Chromium")
return _session
except Exception as exc:
_log.warning("Mermaid Chromium failed: %s", exc)
_session = None
return None
def _render_browser(source: str, *, scale: float = 1.0) -> tuple[str, bytes] | None:
session = _get_session(prefer_system=True)
if session is None:
return None
try:
return session.render_png(source, scale=scale)
except Exception as exc:
_log.warning("Mermaid browser render failed: %s", exc)
reset_mermaid_session()
return None
BackendName = Literal["browser", "quickjs"]
def render_mermaid_local(
source: str,
*,
scale: float = 1.0,
) -> tuple[str, bytes, BackendName] | None:
"""
Try local Mermaid backends in order: system/Chromium browser → QuickJS.
Returns (svg, png_bytes, backend) or None.
"""
got = _render_browser(source, scale=scale)
if got:
return got[0], got[1], "browser"
got = _render_quickjs(source, scale=scale)
if got:
return got[0], got[1], "quickjs"
return None
def write_mermaid_outputs(
source: str,
out_png: Path,
out_svg: Path | None,
*,
scale: float = 1.0,
) -> bool:
"""Render locally and write PNG (+ optional SVG)."""
result = render_mermaid_local(source, scale=scale)
if not result:
return False
svg, png, backend = result
out_png.write_bytes(png)
if out_svg is not None:
out_svg.write_text(svg, encoding="utf-8")
_log.info("Mermaid local ok (%s) → %s", backend, out_png.name)
return True
__all__ = [
"CACHE_KEY_PREFIX",
"chromium_installed",
"detect_system_browser_channel",
"ensure_playwright_browsers_env",
"install_playwright_chromium",
"mermaid_engine_status",
"mermaidx_available",
"playwright_available",
"playwright_browsers_dir",
"prepare_mermaid_source",
"render_mermaid_local",
"reset_mermaid_session",
"write_mermaid_outputs",
]
+226
View File
@@ -0,0 +1,226 @@
"""Post-conversion heuristic: half-empty pages inside an unfinished section.
Requires Windows + Microsoft Word + pywin32. Findings are ALWAYS heuristic —
false positives are expected (large figures, end of subsection, odd breaks).
Never treat as --strict errors.
"""
from __future__ import annotations
from dataclasses import dataclass
# Unused fraction of the text area below the last content on the page.
EMPTY_FRACTION_THRESHOLD = 0.40
FRONT_MATTER_HINTS = (
"СОДЕРЖАНИЕ",
"ТИТУЛ",
"ЗАДАНИЕ",
)
@dataclass
class PageMetric:
"""Synthetic / measured metrics for one page (CI-friendly)."""
page_index: int # 1-based
content_bottom_frac: float # 0..1, how far down the text area content reaches
section_title: str
next_section_title: str | None
is_landscape: bool = False
is_last_doc_page: bool = False
mostly_figure_or_table: bool = False
@dataclass
class PageFillIssue:
page_index: int
message: str
severity: str = "heuristic"
def evaluate_page_fill(
pages: list[PageMetric],
*,
empty_threshold: float = EMPTY_FRACTION_THRESHOLD,
) -> list[PageFillIssue]:
"""Pure heuristic over page metrics (no Word). Safe for unit tests."""
issues: list[PageFillIssue] = []
for i, page in enumerate(pages):
if page.is_landscape:
continue
if page.mostly_figure_or_table:
continue
if page.is_last_doc_page:
continue
title_u = (page.section_title or "").upper()
if any(h in title_u for h in FRONT_MATTER_HINTS):
continue
# Last page of this section (next page starts a different H1)
if page.next_section_title and page.next_section_title != page.section_title:
continue
empty_frac = 1.0 - page.content_bottom_frac
if empty_frac <= empty_threshold:
continue
# Same section continues on the next page
if i + 1 >= len(pages):
continue
nxt = pages[i + 1]
if nxt.section_title != page.section_title:
continue
issues.append(PageFillIssue(
page_index=page.page_index,
message=(
f"[heuristic] page.fill: на стр. {page.page_index} пустой низ "
f"~{empty_frac:.0%} полосы, а раздел «{page.section_title}» "
f"продолжается на следующей странице. "
f"Проверка эвристическая — возможны ложные срабатывания "
f"(крупный рисунок, конец пункта, нестандартный разрыв)."
),
))
return issues
def check_docx_page_fill(path: str) -> tuple[list[PageFillIssue], str]:
"""
Open DOCX in Word, repaginate, collect metrics, evaluate.
Returns (issues, status_message). On missing Word/pywin32 returns ([], reason).
"""
try:
import win32com.client # type: ignore
except ImportError:
return [], (
"Проверка вёрстки пропущена: нужен пакет pywin32 "
"(pip install pywin32) и Microsoft Word."
)
word = None
doc = None
try:
word = win32com.client.DispatchEx("Word.Application")
word.Visible = False
word.DisplayAlerts = 0
abs_path = str(path)
doc = word.Documents.Open(abs_path, ReadOnly=True)
doc.Repaginate()
pages_metrics: list[PageMetric] = []
page_count = int(doc.ComputeStatistics(2)) # wdStatisticPages
for page_no in range(1, page_count + 1):
try:
metric = _measure_page(doc, page_no, page_count)
except Exception:
continue
if metric is not None:
pages_metrics.append(metric)
issues = evaluate_page_fill(pages_metrics)
if not issues:
return [], (
"Проверка вёрстки (Word): замечаний по полупустым страницам нет "
"(эвристика; возможны пропуски)."
)
return issues, (
f"Проверка вёрстки (Word): найдено замечаний — {len(issues)} "
f"(все эвристические, могут быть ложными)."
)
except Exception as exc:
return [], f"Проверка вёрстки пропущена: не удалось открыть Word ({exc})."
finally:
try:
if doc is not None:
doc.Close(False)
except Exception:
pass
try:
if word is not None:
word.Quit()
except Exception:
pass
def _measure_page(doc, page_no: int, page_count: int) -> PageMetric | None:
"""Best-effort measurement via Word COM selection / page setup."""
selection = doc.Application.Selection
selection.GoTo(What=1, Which=1, Count=page_no) # wdGoToPage, wdGoToAbsolute
section = selection.Sections(1)
ps = section.PageSetup
is_landscape = bool(int(ps.Orientation) == 1) # wdOrientLandscape
page_h = float(ps.PageHeight)
top = float(ps.TopMargin)
bottom = float(ps.BottomMargin)
text_h = max(page_h - top - bottom, 1.0)
start = int(selection.Start)
if page_no < page_count:
selection.GoTo(What=1, Which=1, Count=page_no + 1)
end = int(selection.Start) - 1
else:
end = int(doc.Content.End)
if end < start:
end = start
rng = doc.Range(start, end)
try:
# wdVerticalPositionRelativeToPage = 6
vpos = float(rng.Information(6))
content_bottom = max(0.0, min(1.0, (vpos - top) / text_h))
except Exception:
content_bottom = 1.0
section_title = _heading_near(doc, start)
next_title = None
if page_no < page_count:
try:
selection.GoTo(What=1, Which=1, Count=page_no + 1)
next_title = _heading_near(doc, int(selection.Start))
except Exception:
next_title = None
mostly_object = False
try:
text_len = len((rng.Text or "").strip())
if rng.Tables.Count >= 1 and text_len < 80:
mostly_object = True
if rng.InlineShapes.Count >= 1 and text_len < 80:
mostly_object = True
except Exception:
pass
return PageMetric(
page_index=page_no,
content_bottom_frac=content_bottom,
section_title=section_title or "",
next_section_title=next_title,
is_landscape=is_landscape,
is_last_doc_page=(page_no == page_count),
mostly_figure_or_table=mostly_object,
)
def _heading_near(doc, pos: int) -> str:
"""Walk backwards for nearest Heading 1 style paragraph."""
try:
p = doc.Range(pos, pos).Paragraphs(1)
for _ in range(80):
style = str(p.Style)
if "Heading 1" in style or "Заголовок 1" in style:
return (p.Range.Text or "").strip().replace("\r", "")
if p.Range.Start <= 1:
break
p = p.Previous()
if p is None:
break
except Exception:
pass
return ""
def format_page_fill_report(issues: list[PageFillIssue], status: str) -> str:
lines = [status]
for i in issues:
lines.append(f" [{i.severity}] page:{i.page_index}: {i.message}")
return "\n".join(lines)
+303
View File
@@ -0,0 +1,303 @@
"""A4 page geometry helpers for portrait / landscape sections."""
from __future__ import annotations
import os
import tempfile
import zipfile
from pathlib import Path
from docx.enum.section import WD_ORIENT
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
from docx.oxml.ns import qn
from docx.shared import Mm, Pt
from lxml import etree
from .util import create_element
_W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
_W = f"{{{_W_NS}}}"
# GOST-like A4 margins (same as styles._apply_common_page_and_body)
MARGIN_LEFT = Mm(30)
MARGIN_RIGHT = Mm(10)
MARGIN_TOP = Mm(20)
MARGIN_BOTTOM = Mm(20)
# mirea / pis_custom — 30/10/20/20; paco_custom — форма ПАЦО 20/20/20/20
MARGIN_PRESETS = {
"mirea": (Mm(30), Mm(10), Mm(20), Mm(20)),
"pis_custom": (Mm(30), Mm(10), Mm(20), Mm(20)),
}
def apply_margin_preset(preset: str) -> None:
"""Switch active page margins used by apply_section_geometry / content_size."""
global MARGIN_LEFT, MARGIN_RIGHT, MARGIN_TOP, MARGIN_BOTTOM
left, right, top, bottom = MARGIN_PRESETS.get(preset, MARGIN_PRESETS["mirea"])
MARGIN_LEFT = left
MARGIN_RIGHT = right
MARGIN_TOP = top
MARGIN_BOTTOM = bottom
A4_SHORT = Mm(210)
A4_LONG = Mm(297)
def is_landscape_section(section) -> bool:
"""True if section is (or should be treated as) landscape A4."""
try:
if section.orientation == WD_ORIENT.LANDSCAPE:
return True
except Exception:
pass
return int(section.page_width) > int(section.page_height)
def apply_section_geometry(section, *, landscape: bool) -> None:
"""
Set orientation and page size.
Set orientation first (python-docx may swap w/h on change), then force A4 dims.
"""
target = WD_ORIENT.LANDSCAPE if landscape else WD_ORIENT.PORTRAIT
try:
section.orientation = target
except Exception:
pass
if landscape:
section.page_width = A4_LONG
section.page_height = A4_SHORT
else:
section.page_width = A4_SHORT
section.page_height = A4_LONG
section.left_margin = MARGIN_LEFT
section.right_margin = MARGIN_RIGHT
section.top_margin = MARGIN_TOP
section.bottom_margin = MARGIN_BOTTOM
# Explicit orient on pgSz for Word.
pg_sz = section._sectPr.find(qn("w:pgSz"))
if pg_sz is None:
pg_sz = section._sectPr._add_pgSz()
if landscape:
pg_sz.set(qn("w:orient"), "landscape")
# Re-assert after XML tweak (some builds reshuffle).
section.page_width = A4_LONG
section.page_height = A4_SHORT
else:
if pg_sz.get(qn("w:orient")) is not None:
del pg_sz.attrib[qn("w:orient")]
section.page_width = A4_SHORT
section.page_height = A4_LONG
# Vertical align: center on landscape (figures/tables only); top on portrait
sect_pr = section._sectPr
for el in list(sect_pr.findall(qn("w:vAlign"))):
sect_pr.remove(el)
if landscape:
sect_pr.append(create_element("w:vAlign", {"w:val": "center"}))
# add_section copies template sectPr including w:pgNumType/@w:start — strip so
# landscape / return-to-portrait sections continue numbering instead of restarting.
clear_section_page_start(section)
def content_size(*, landscape: bool) -> tuple:
"""Return (max_height, max_width) usable content area for LayoutTracker."""
if landscape:
page_w, page_h = A4_LONG, A4_SHORT
else:
page_w, page_h = A4_SHORT, A4_LONG
max_height = page_h - MARGIN_TOP - MARGIN_BOTTOM
max_width = page_w - MARGIN_LEFT - MARGIN_RIGHT
return max_height, max_width
def apply_centered_page_footer(section) -> None:
"""Centered PAGE field, Times New Roman 12 (body section style)."""
footer = section.footer
footer.is_linked_to_previous = False
if not footer.paragraphs:
footer.add_paragraph()
paragraph = footer.paragraphs[0]
paragraph.clear()
paragraph.paragraph_format.first_line_indent = 0
paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
run = paragraph.add_run()
run.font.name = "Times New Roman"
run.font.size = Pt(12)
paragraph._p.append(create_element("w:fldSimple", {
"w:instr": "PAGE \\* MERGEFORMAT",
}))
def clear_section_footer(section) -> None:
"""Empty footer (no PAGE) — for title / assignment / TOC sections."""
footer = section.footer
footer.is_linked_to_previous = False
for p in footer.paragraphs:
p.clear()
if not footer.paragraphs:
footer.add_paragraph()
def clear_section_page_start(section) -> None:
"""Remove w:pgNumType/@w:start so PAGE continues from the previous section."""
sect_pr = section._sectPr
for child in list(sect_pr):
if child.tag != qn("w:pgNumType"):
continue
if child.get(qn("w:start")) is not None:
del child.attrib[qn("w:start")]
# Empty pgNumType (no start/fmt) is useless — drop the element.
if len(child.attrib) == 0:
sect_pr.remove(child)
def ensure_continuous_page_numbers(document) -> None:
"""
After docxcompose of title/assignment: no PAGE on early sections,
continuous numbering (do not restart at 1 on body section).
"""
sections = list(document.sections)
if not sections:
return
# Strip w:pgNumType start everywhere so landscape / body sections stay continuous.
for section in sections:
clear_section_page_start(section)
def set_section_page_start(section, start: int) -> None:
"""Set w:pgNumType/@w:start so PAGE field begins at ``start`` in this section."""
if start < 1:
return
sect_pr = section._sectPr
pg = sect_pr.find(qn("w:pgNumType"))
if pg is None:
sect_pr.append(create_element("w:pgNumType", {"w:start": str(int(start))}))
else:
pg.set(qn("w:start"), str(int(start)))
def apply_page_number_start(
document,
start: int | None,
*,
front_sections: int = 0,
) -> None:
"""
Apply page-number offset for the body.
``start`` is None / <1 — leave continuous numbering (strip explicit starts).
``start`` >= 1 — first section after front matter restarts PAGE at that number;
later body sections stay continuous from there.
"""
ensure_continuous_page_numbers(document)
if start is None or int(start) < 1:
return
sections = list(document.sections)
idx = max(0, int(front_sections))
if idx >= len(sections):
return
set_section_page_start(sections[idx], int(start))
def _iter_sect_pr(document_root) -> list:
"""Return sectPr elements in document order (inline + final body sectPr)."""
body = document_root.find(f"{_W}body")
if body is None:
return []
sects: list = []
for child in body:
tag = child.tag
if tag == f"{_W}sectPr":
sects.append(child)
continue
if tag != f"{_W}p":
continue
p_pr = child.find(f"{_W}pPr")
if p_pr is None:
continue
sect = p_pr.find(f"{_W}sectPr")
if sect is not None:
sects.append(sect)
return sects
def _strip_pg_num_start_xml(sect_pr) -> None:
for child in list(sect_pr):
if child.tag != f"{_W}pgNumType":
continue
if child.get(f"{_W}start") is not None:
del child.attrib[f"{_W}start"]
if len(child.attrib) == 0:
sect_pr.remove(child)
def _set_pg_num_start_xml(sect_pr, start: int) -> None:
pg = sect_pr.find(f"{_W}pgNumType")
if pg is None:
pg = etree.SubElement(sect_pr, f"{_W}pgNumType")
pg.set(f"{_W}start", str(int(start)))
def patch_docx_page_starts(
path: str | Path,
start: int | None = None,
*,
front_sections: int = 0,
) -> None:
"""
Strip (and optionally re-apply) w:pgNumType/@w:start inside word/document.xml.
Used after Word COM Save, which often rewrites start=\"1\" onto every section.
Zip/lxml only — does not round-trip through python-docx (preserves
continuation captions and other Word-written parts).
"""
abs_path = os.path.abspath(str(path))
if not os.path.isfile(abs_path):
return
with zipfile.ZipFile(abs_path, "r") as zin:
names = zin.namelist()
if "word/document.xml" not in names:
return
doc_bytes = zin.read("word/document.xml")
other = {n: zin.read(n) for n in names if n != "word/document.xml"}
root = etree.fromstring(doc_bytes)
sects = _iter_sect_pr(root)
for sect in sects:
_strip_pg_num_start_xml(sect)
if start is not None and int(start) >= 1:
idx = max(0, int(front_sections))
if idx < len(sects):
_set_pg_num_start_xml(sects[idx], int(start))
new_doc = etree.tostring(
root,
xml_declaration=True,
encoding="UTF-8",
standalone=True,
)
parent = os.path.dirname(abs_path) or "."
fd, tmp_name = tempfile.mkstemp(suffix=".docx", dir=parent)
os.close(fd)
try:
with zipfile.ZipFile(tmp_name, "w", compression=zipfile.ZIP_DEFLATED) as zout:
for name, data in other.items():
zout.writestr(name, data)
zout.writestr("word/document.xml", new_doc)
os.replace(tmp_name, abs_path)
except Exception:
try:
os.remove(tmp_name)
except OSError:
pass
raise
+8 -3
View File
@@ -1,7 +1,7 @@
from collections.abc import Generator from collections.abc import Generator
from docx import Document from docx import Document
from marko.block import BlankLine from marko.block import BlankLine, ThematicBreak
from .extended_markdown import markdown, Caption from .extended_markdown import markdown, Caption
from .renderable.caption import CaptionInfo from .renderable.caption import CaptionInfo
@@ -12,13 +12,14 @@ from .renderable_factory import RenderableFactory
class Parser: class Parser:
"""Parses given markdown string and returns Renderable elements""" """Parses given markdown string and returns Renderable elements"""
def __init__(self, document: Document, text: str): def __init__(self, document: Document, text: str, hr_pagebreak: bool = False):
self._document = document self._document = document
self._parsed = markdown.parse(text) self._parsed = markdown.parse(text)
self._caption_info: CaptionInfo | None = None self._caption_info: CaptionInfo | None = None
self._hr_pagebreak = hr_pagebreak
def parse(self) -> Generator[Renderable, None, None]: def parse(self) -> Generator[Renderable, None, None]:
factory = RenderableFactory(self._document._body) factory = RenderableFactory(self._document._body, hr_pagebreak=self._hr_pagebreak)
for marko_element in self._parsed.children: for marko_element in self._parsed.children:
if isinstance(marko_element, BlankLine): if isinstance(marko_element, BlankLine):
@@ -29,8 +30,12 @@ class Parser:
marko_element.unique_name, marko_element.unique_name,
marko_element.text, marko_element.text,
getattr(marko_element, "with_listing", False), getattr(marko_element, "with_listing", False),
getattr(marko_element, "landscape", False),
) )
continue continue
if isinstance(marko_element, ThematicBreak) and not self._hr_pagebreak:
continue
yield factory.create(marko_element, self._caption_info) yield factory.create(marko_element, self._caption_info)
self._caption_info = None self._caption_info = None
+490
View File
@@ -0,0 +1,490 @@
"""Shared Markdown → DOCX pipeline for CLI and GUI."""
from __future__ import annotations
import logging
import os
import platform
import subprocess
import traceback
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Callable
from docx import Document
from . import package_dir
from .checker import check_markdown, format_report
from .converter import Converter
from .profiles import (
DEFAULT_HEADING_NUMBERING,
DEFAULT_TABLE_CONTINUATION,
DEFAULT_LISTING_CONTINUATION,
DEFAULT_TOC_MODE,
)
LogFn = Callable[[str], None]
@dataclass
class ConvertRequest:
filename: str = ""
output: str | None = None
template: str | None = None
doc_type: str = "practice"
heading_numbering: str = DEFAULT_HEADING_NUMBERING
toc_mode: str = DEFAULT_TOC_MODE
table_continuation: str = DEFAULT_TABLE_CONTINUATION
listing_continuation: str = DEFAULT_LISTING_CONTINUATION
emdash_to_hyphen: bool = False
hr_pagebreak: bool = False
title: str | None = None
assignment: str | None = None
student: str | None = None
group: str | None = None
# Auto-build title from info_conv.yaml / ```title (off by default).
auto_title: bool = False
check: bool = False
check_only: bool = False
strict: bool = False
syntax_highlighting: bool = False
plantuml_jar: str | None = None
kroki_url: str | None = None
diagram_fallback: str = "remote"
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
table_repeat_header: bool = False
# PAGE field start on body section; None = continuous from document start.
page_number_start: int | None = None
# DOCX File → Info; None = take from md2gost.user.json (see doc_metadata).
doc_author_source: str | None = None
doc_author: str | None = None
doc_last_modified_by: str | None = None
doc_title: str | None = None
doc_subject: str | None = None
doc_keywords: str | None = None
doc_comments: str | None = None
doc_category: str | None = None
@dataclass
class ConvertResult:
ok: bool
exit_code: int = 0
output_path: str | None = None
check_report: str = ""
message: str = ""
def default_output_path(filename: str) -> str:
base = os.path.basename(filename)
if base.lower().endswith(".md"):
base = base[:-3]
return os.path.join(os.path.dirname(os.path.abspath(filename)), base + ".docx")
def timestamped_output_path(path: str) -> str:
"""report.docx → report_2026-09-05-10-05.docx (local time; seconds if taken)."""
directory, name = os.path.split(os.path.abspath(path))
stem, ext = os.path.splitext(name)
if ext.lower() != ".docx":
ext = ".docx"
stamp = datetime.now().strftime("%Y-%m-%d-%H-%M")
candidate = os.path.join(directory, f"{stem}_{stamp}{ext}")
if not os.path.exists(candidate):
return candidate
stamp = datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
candidate = os.path.join(directory, f"{stem}_{stamp}{ext}")
n = 2
while os.path.exists(candidate):
candidate = os.path.join(directory, f"{stem}_{stamp}_{n}{ext}")
n += 1
return candidate
def default_template_path() -> str:
return os.path.join(package_dir(), "Template.docx")
def open_document(path: str) -> None:
system = platform.system()
if system == "Darwin":
subprocess.call(("open", path))
elif system == "Windows":
os.startfile(path) # type: ignore[attr-defined]
else:
subprocess.call(("xdg-open", path))
def _fix_front_matter_after_compose(document, *, had_title: bool, had_assignment: bool) -> None:
"""Clear PAGE on title/assignment sections; keep continuous page numbers."""
from .page_geometry import clear_section_footer, ensure_continuous_page_numbers
ensure_continuous_page_numbers(document)
n_front = int(bool(had_title)) + int(bool(had_assignment))
for i, section in enumerate(document.sections):
if i < n_front:
clear_section_footer(section)
else:
break
ensure_continuous_page_numbers(document)
def _apply_page_offset(document, req: ConvertRequest, *, had_title: bool, had_assignment: bool) -> None:
from .page_geometry import apply_page_number_start
start = getattr(req, "page_number_start", None)
start_i: int | None
if start is None:
start_i = None
else:
try:
start_i = int(start)
except (TypeError, ValueError):
start_i = None
front = int(bool(had_title)) + int(bool(had_assignment))
# Always strip copied w:pgNumType/@w:start (landscape sections); optional body restart.
apply_page_number_start(document, start_i, front_sections=front)
def _page_offset_args(req: ConvertRequest, *, had_title: bool, had_assignment: bool) -> tuple[int | None, int]:
"""Return (page_number_start, front_sections) for post-Word patch."""
start = getattr(req, "page_number_start", None)
if start is None:
start_i = None
else:
try:
start_i = int(start)
except (TypeError, ValueError):
start_i = None
front = int(bool(had_title)) + int(bool(had_assignment))
return start_i, front
def convert(req: ConvertRequest, log: LogFn | None = None) -> ConvertResult:
"""Run check and/or conversion. Does not re-raise; errors go into ConvertResult."""
emit: LogFn = log if log is not None else print
def fail(code: int, message: str, check_report: str = "") -> ConvertResult:
emit(message)
return ConvertResult(False, code, check_report=check_report, message=message)
filename = (req.filename or "").strip()
# Standalone page-fill check on an existing DOCX
if req.check_pages and filename.lower().endswith(".docx") and not filename.lower().endswith(".md"):
from .page_fill_check import check_docx_page_fill, format_page_fill_report
issues, status = check_docx_page_fill(filename)
report = format_page_fill_report(issues, status)
emit(report)
return ConvertResult(True, 0, check_report=report, message=report)
# DOCX → Markdown (word2md) when input is .docx without --check-pages
if filename.lower().endswith(".docx"):
from word2md import ImportRequest, convert_docx
out = (req.output or "").strip() or None
if out and out.lower().endswith(".docx"):
out = str(Path(filename).with_suffix(".md"))
result = convert_docx(
ImportRequest(filename=filename, output=out),
log=emit,
)
return ConvertResult(
result.ok,
result.exit_code,
output_path=result.output_path,
message=result.message,
)
if not filename:
return fail(2, "Укажите исходный markdown-файл")
if not filename.lower().endswith(".md"):
return fail(1, "Исходный файл должен быть в формате .md")
if not os.path.isfile(filename):
return fail(2, f"Файл не найден: {filename}")
output = (req.output or "").strip() or None
if output and not output.lower().endswith(".docx"):
return fail(1, "Выходной файл должен быть в формате .docx")
for label, path in (("титул", req.title), ("задание", req.assignment), ("шаблон", req.template)):
if path and not os.path.isfile(path):
return fail(2, f"Файл ({label}) не найден: {path}")
if req.syntax_highlighting:
os.environ["SYNTAX_HIGHLIGHTING"] = "1"
else:
os.environ.pop("SYNTAX_HIGHLIGHTING", None)
from .diagram_renderer import configure_diagrams
md_dir = os.path.dirname(os.path.abspath(filename)) or "."
configure_diagrams(
plantuml_jar=req.plantuml_jar or None,
kroki_url=req.kroki_url or None,
fallback=req.diagram_fallback,
schemes_path=req.schemes_path or None,
md_dir=md_dir,
diagram_format=req.diagram_format if req.diagram_format in ("png", "svg") else "png",
diagram_scale=req.diagram_scale,
)
os.environ["WORKING_DIR"] = md_dir
with open(filename, encoding="utf-8") as f:
md_text = f.read()
from .title_page import (
build_title_docx,
load_info_conv,
merge_title_meta,
parse_and_strip_title_fence,
should_generate_title,
)
from .user_profile import load_user_profile
fence_fields, body_md = parse_and_strip_title_fence(md_text)
yaml_fields = load_info_conv(md_dir)
generated_title_path: str | None = None
title_path = (req.title or "").strip() or None
if not output:
output = default_output_path(filename)
if should_generate_title(
explicit_title=title_path,
fence_fields=fence_fields,
md_dir=md_dir,
enabled=bool(req.auto_title),
):
profile = load_user_profile()
student = (req.student or "").strip() or profile.student
group = (req.group or "").strip() or profile.group
if not student or not group:
return fail(
2,
"Для генерации титула нужны ФИО и группа студента "
"(GUI: Настройки → Студент…, или CLI: --student / --group).",
)
meta = merge_title_meta(
yaml_fields=yaml_fields,
fence_fields=fence_fields,
student=student,
group=group,
)
out_dir = os.path.dirname(os.path.abspath(output)) or md_dir
generated_title_path = os.path.join(out_dir, ".md2gost_title.docx")
try:
build_title_docx(meta, generated_title_path)
title_path = generated_title_path
emit(f"Титульный лист: {generated_title_path}")
except Exception as exc:
return fail(1, f"Не удалось сгенерировать титул: {exc}")
check_report = ""
if req.check or req.check_only:
issues = check_markdown(
body_md,
req.doc_type,
table_continuation=req.table_continuation,
listing_continuation=req.listing_continuation,
syntax_highlighting=req.syntax_highlighting,
strict=req.strict,
)
check_report = format_report(issues)
emit(check_report)
errors = [i for i in issues if i.severity == "error"]
if req.strict and errors:
return fail(1, "Проверка ТЗ: есть ошибки (--strict)", check_report)
if req.check_only:
return ConvertResult(
True, 0, check_report=check_report,
message=check_report,
)
template = (req.template or "").strip() or default_template_path()
handler = _CallbackLogHandler(emit)
handler.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
root_log = logging.getLogger("md2gost")
root_log.addHandler(handler)
attached_diag = logging.getLogger("md2gost.diagram_renderer")
prev_diag_level = attached_diag.level
attached_diag.setLevel(logging.INFO)
attached_diag.addHandler(handler)
try:
converter = Converter(
filename, template, req.debug,
doc_type=req.doc_type,
heading_numbering=req.heading_numbering,
emdash_to_hyphen=req.emdash_to_hyphen,
toc_mode=req.toc_mode,
table_continuation=req.table_continuation,
listing_continuation=req.listing_continuation,
hr_pagebreak=req.hr_pagebreak,
styles_path=req.styles_path or None,
markdown_text=body_md,
)
converter.convert()
document = converter.document
if title_path or req.assignment:
try:
from docxcompose.composer import Composer
except ImportError:
return fail(3, "Для титула/задания нужен пакет docxcompose", check_report)
from .styles import apply_style_config
style_cfg = converter.style_config
shell = Document(template)
apply_style_config(shell, style_cfg)
body = shell.element.body
for child in list(body):
if not child.tag.endswith("}sectPr"):
body.remove(child)
composer = Composer(shell)
if title_path:
composer.append(Document(title_path))
shell.add_page_break()
if req.assignment:
composer.append(Document(req.assignment))
shell.add_page_break()
composer.append(document)
document = composer.doc
apply_style_config(document, style_cfg)
_fix_front_matter_after_compose(
document,
had_title=bool(title_path),
had_assignment=bool(req.assignment),
)
_apply_page_offset(
document,
req,
had_title=bool(title_path),
had_assignment=bool(req.assignment),
)
from .doc_metadata import apply_document_metadata, resolve_document_metadata
from .user_profile import load_user_profile as _load_user_profile
profile = _load_user_profile()
apply_document_metadata(
document,
resolve_document_metadata(
stored=profile.metadata,
student=(req.student or "").strip() or profile.student,
author_source=req.doc_author_source,
author=req.doc_author,
last_modified_by=req.doc_last_modified_by,
title=req.doc_title,
subject=req.doc_subject,
keywords=req.doc_keywords,
comments=req.doc_comments,
category=req.doc_category,
),
)
try:
document.save(output)
except PermissionError:
alt = timestamped_output_path(output)
emit(f"Не удалось записать {output} (файл занят). Сохраняю как {alt}")
document.save(alt)
output = alt
except Exception as exc:
emit(traceback.format_exc())
return ConvertResult(
False, 1, check_report=check_report,
message=f"Ошибка конвертации: {exc}",
)
finally:
# Headless Chrome would otherwise stay alive until the next conversion.
try:
from .diagram_renderer import shutdown_diagrams
shutdown_diagrams()
except Exception:
pass
root_log.removeHandler(handler)
attached_diag.removeHandler(handler)
attached_diag.setLevel(prev_diag_level)
if generated_title_path and os.path.isfile(generated_title_path):
try:
os.remove(generated_title_path)
except OSError:
pass
abs_out = os.path.abspath(output)
emit(f"Generated document: {abs_out}")
if req.table_continuation == "word" or req.listing_continuation == "word":
from .word_fix import fix_continuations
from .page_geometry import patch_docx_page_starts
fix = fix_continuations(
abs_out,
tables=(req.table_continuation == "word"),
listings=(req.listing_continuation == "word"),
repeat_header=bool(req.table_repeat_header),
)
emit(fix.message)
if fix.details:
for line in fix.details:
emit(line)
if not fix.ok:
check_report = (
(check_report + "\n" + fix.message).strip() if check_report else fix.message
)
# Word Save often rewrites w:pgNumType/@w:start=\"1\" on every section — strip again.
start_i, front = _page_offset_args(
req,
had_title=bool(title_path),
had_assignment=bool(req.assignment),
)
try:
patch_docx_page_starts(abs_out, start_i, front_sections=front)
except Exception as exc:
emit(f"Не удалось восстановить сквозную нумерацию после Word: {exc}")
if req.check_pages:
from .page_fill_check import check_docx_page_fill, format_page_fill_report
issues, status = check_docx_page_fill(abs_out)
page_report = format_page_fill_report(issues, status)
emit(page_report)
check_report = (check_report + "\n" + page_report).strip() if check_report else page_report
if req.debug or req.open_when_done:
try:
open_document(abs_out)
except Exception as exc:
emit(f"Не удалось открыть файл: {exc}")
return ConvertResult(
True, 0, output_path=abs_out, check_report=check_report,
message=f"Generated document: {abs_out}",
)
class _CallbackLogHandler(logging.Handler):
def __init__(self, emit: LogFn):
super().__init__()
self._emit = emit
def emit(self, record: logging.LogRecord) -> None:
try:
self._emit(self.format(record))
except Exception:
pass
def should_launch_gui(filename: str | None, gui_flag: bool) -> bool:
"""GUI if --gui, or if no input file was given (interactive default)."""
return bool(gui_flag or not filename)
+48 -9
View File
@@ -19,12 +19,17 @@ DEFAULT_HEADING_NUMBERING = "manual"
TOC_MODES = ("manual", "native") TOC_MODES = ("manual", "native")
DEFAULT_TOC_MODE = "native" DEFAULT_TOC_MODE = "native"
# off — одна таблица Word, пагинацию делает Word (по умолчанию; без автоподписи) # off — одна таблица Word, пагинацию делает Word (без автоподписи)
# legacy — режем по оценке высоты + «Продолжение…» с page_break_before # legacy — режем по оценке высоты + «Продолжение…» с page_break_before
# caption — режем по оценке + явный PageBreak + «Продолжение…» # caption — режем по оценке + явный PageBreak + «Продолжение…»
# soft — синоним off (автоподпись mid-page без точной вёрстки Word невозможна в DOCX) # soft — синоним off (автоподпись mid-page без точной вёрстки Word невозможна в DOCX)
TABLE_CONTINUATION_MODES = ("off", "legacy", "caption", "soft") # word — как off при рендере; после save Word COM режет по реальной пагинации + «Продолжение…»
DEFAULT_TABLE_CONTINUATION = "off" TABLE_CONTINUATION_MODES = ("off", "legacy", "caption", "soft", "word")
DEFAULT_TABLE_CONTINUATION = "word"
# Same modes as tables (word = post-process via Word COM after save).
LISTING_CONTINUATION_MODES = TABLE_CONTINUATION_MODES
DEFAULT_LISTING_CONTINUATION = DEFAULT_TABLE_CONTINUATION
# section — Рисунок 1.1 / 2.1 (ТЗ МИРЭА); continuous — Рисунок 1, 2, 3 (ПИС) # section — Рисунок 1.1 / 2.1 (ТЗ МИРЭА); continuous — Рисунок 1, 2, 3 (ПИС)
NUMBERING_SCOPES = ("section", "continuous") NUMBERING_SCOPES = ("section", "continuous")
@@ -94,6 +99,38 @@ def get_profile(doc_type: str) -> DocProfile:
# --- text autofixes --- # --- text autofixes ---
# ```uml / ```mermaid / ```uml-c4 … — кавычки и тире там синтаксис
_FENCE_RE = re.compile(
r"^(?P<fence>`{3,}|~{3,})(?P<info>[^\n]*)\n"
r"(?P<body>[\s\S]*?)"
r"^(?P=fence)[ \t]*(?:\n|$)",
re.M,
)
def _fence_lang(info: str) -> str:
return (info.strip().split() or [""])[0].lower()
def _is_diagram_fence_info(info: str) -> bool:
from .diagram_schemes import is_diagram_lang
return is_diagram_lang(_fence_lang(info))
def _map_outside_diagram_fences(text: str, fn) -> str:
"""Apply fn only outside UML / Mermaid / scheme fences."""
out: list[str] = []
pos = 0
for m in _FENCE_RE.finditer(text):
if not _is_diagram_fence_info(m.group("info")):
continue
out.append(fn(text[pos:m.start()]))
out.append(m.group(0))
pos = m.end()
out.append(fn(text[pos:]))
return "".join(out)
def fix_russian_quotes(text: str) -> str: def fix_russian_quotes(text: str) -> str:
"""Replace "..." with «...» for Russian text segments (heuristic).""" """Replace "..." with «...» for Russian text segments (heuristic)."""
def repl(m): def repl(m):
@@ -137,12 +174,14 @@ def dash_separator() -> str:
def preprocess_markdown(text: str, emdash_to_hyphen: bool | None = None) -> str: def preprocess_markdown(text: str, emdash_to_hyphen: bool | None = None) -> str:
if emdash_to_hyphen is not None: if emdash_to_hyphen is not None:
set_emdash_to_hyphen(emdash_to_hyphen) set_emdash_to_hyphen(emdash_to_hyphen)
text = fix_russian_quotes(text)
if _EMDASH_TO_HYPHEN: def _apply(chunk: str) -> str:
# Keep ASCII hyphens; flatten any em dashes from the source chunk = fix_russian_quotes(chunk)
text = replace_emdash_with_hyphen(text) if _EMDASH_TO_HYPHEN:
else: return replace_emdash_with_hyphen(chunk)
text = fix_dashes(text) return fix_dashes(chunk)
text = _map_outside_diagram_fences(text, _apply)
text = separate_biblio_lines(text) text = separate_biblio_lines(text)
return text return text
+34 -16
View File
@@ -9,14 +9,14 @@ from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
from md2gost.layout_tracker import LayoutState from md2gost.layout_tracker import LayoutState
from md2gost.renderable import Renderable from md2gost.renderable import Renderable
from md2gost.rendered_info import RenderedInfo from md2gost.rendered_info import RenderedInfo
from .paragraph_sizer import ParagraphSizer from .paragraph_sizer import ParagraphSizerResult, measure_paragraph
from ..util import create_element from ..util import create_element
# Map category → Word style name # Map category → Word style name
CAPTION_STYLES = { CAPTION_STYLES = {
"Рисунок": "Caption Figure", "Рисунок": "Caption Figure",
"Таблица": "Caption Table", "Таблица": "Название таблицы",
"Листинг": "Caption Listing", "Листинг": "Caption Listing",
} }
@@ -26,6 +26,7 @@ class CaptionInfo:
unique_name: str | None unique_name: str | None
text: str | None text: str | None
with_listing: bool = False with_listing: bool = False
landscape: bool = False
class Caption(Renderable): class Caption(Renderable):
@@ -34,13 +35,24 @@ class Caption(Renderable):
self._parent = parent self._parent = parent
self._before = before self._before = before
self._category = category self._category = category
self._size_cache: dict = {}
self._docx_paragraph = DocxParagraph(create_element("w:p"), parent) self._docx_paragraph = DocxParagraph(create_element("w:p"), parent)
# Table/listing captions use keep_with_next. pageBreakBefore + keepWithNext
# on a paragraph that Word already wraps to a new page produces a blank
# page that the layout tracker / debug overlay never sees. Written here
# rather than at the end of render() so that measuring the caption twice
# (fit check, then emit) sees the same paragraph.
self._docx_paragraph.paragraph_format.page_break_before = False
style_name = CAPTION_STYLES.get(category, "Caption") style_name = CAPTION_STYLES.get(category, "Caption")
try: try:
self._docx_paragraph.style = style_name self._docx_paragraph.style = style_name
except KeyError: except KeyError:
self._docx_paragraph.style = "Caption" try:
self._docx_paragraph.style = "Caption Table" if category == "Таблица" else "Caption"
except KeyError:
self._docx_paragraph.style = "Caption"
# Format: «Рисунок 1.1 — Название» (or « - » if --emdash-to-hyphen) # Format: «Рисунок 1.1 — Название» (or « - » if --emdash-to-hyphen)
from ..profiles import dash_separator from ..profiles import dash_separator
@@ -54,23 +66,29 @@ class Caption(Renderable):
def center(self): def center(self):
self._docx_paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER self._docx_paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
def measure(self, previous_paragraph: DocxParagraph | None, max_width) -> ParagraphSizerResult:
"""Line count and spacing of the caption line against a given predecessor."""
return measure_paragraph(
self._size_cache, self._docx_paragraph, previous_paragraph, max_width
)
def render(self, previous_rendered: RenderedInfo, layout_state: LayoutState) -> Generator[ def render(self, previous_rendered: RenderedInfo, layout_state: LayoutState) -> Generator[
"RenderedInfo | Renderable", None, None]: "RenderedInfo | Renderable", None, None]:
height_data = ParagraphSizer( height_data = self.measure(
self._docx_paragraph,
previous_rendered.docx_element previous_rendered.docx_element
if previous_rendered and isinstance(previous_rendered.docx_element, DocxParagraph) else None, if previous_rendered and isinstance(previous_rendered.docx_element, DocxParagraph) else None,
layout_state.max_width layout_state.max_width
).calculate_height() )
if self._before and ((height_data.lines + 2 - 1) * height_data.line_spacing + 1) * height_data.line_height \ leftover = 0
> layout_state.remaining_page_height: need_new_page = (
self._docx_paragraph.paragraph_format.page_break_before = True self._before
height_data = ParagraphSizer( and layout_state.current_page_height > 0
self._docx_paragraph, and ((height_data.lines + 2 - 1) * height_data.line_spacing + 1) * height_data.line_height
None, > layout_state.remaining_page_height
layout_state.max_width )
).calculate_height() if need_new_page:
leftover = layout_state.remaining_page_height
height_data = self.measure(None, layout_state.max_width)
yield RenderedInfo(self._docx_paragraph, height_data.full + (layout_state.remaining_page_height yield RenderedInfo(self._docx_paragraph, height_data.full + leftover)
if self._docx_paragraph.paragraph_format.page_break_before else 0))
+14 -4
View File
@@ -8,13 +8,13 @@ from .image import Image
from .listing import Listing from .listing import Listing
from .renderable import Renderable from .renderable import Renderable
from .requires_numbering import RequiresNumbering from .requires_numbering import RequiresNumbering
from ..diagram_renderer import render_diagram from ..diagram_renderer import register_pending_diagram, render_diagram
from ..layout_tracker import LayoutState from ..layout_tracker import LayoutState
from ..rendered_info import RenderedInfo from ..rendered_info import RenderedInfo
class DiagramFigure(Renderable, RequiresNumbering): class DiagramFigure(Renderable, RequiresNumbering):
"""UML/BPMN/C4 fence → PNG figure (+ optional source listing).""" """UML / Mermaid / scheme fence → figure (+ optional source listing)."""
def __init__( def __init__(
self, self,
@@ -34,6 +34,10 @@ class DiagramFigure(Renderable, RequiresNumbering):
) )
self._number = None self._number = None
self._listing_number = None self._listing_number = None
# Parsing runs fully before rendering, so this collects the whole document
# and lets the first render() draw every PlantUML source in one JVM.
register_pending_diagram(lang, source)
self.landscape = bool(caption_info and caption_info.landscape)
if caption_info and caption_info.unique_name: if caption_info and caption_info.unique_name:
self.unique_name = caption_info.unique_name self.unique_name = caption_info.unique_name
@@ -56,8 +60,14 @@ class DiagramFigure(Renderable, RequiresNumbering):
def render(self, previous_rendered: RenderedInfo, layout_state: LayoutState) \ def render(self, previous_rendered: RenderedInfo, layout_state: LayoutState) \
-> Generator[RenderedInfo, None, None]: -> Generator[RenderedInfo, None, None]:
png_path = render_diagram(self._lang, self._source) result = render_diagram(self._lang, self._source)
image = Image(self._parent, png_path, self._caption_info) image = Image(
self._parent,
result.png_path,
self._caption_info,
svg_path=result.svg_path,
pixel_scale=result.pixel_scale,
)
if self._number is not None: if self._number is not None:
image.set_number(self._number) image.set_number(self._number)
yield from image.render(previous_rendered, layout_state) yield from image.render(previous_rendered, layout_state)
+2 -2
View File
@@ -2,7 +2,6 @@ from copy import copy
from typing import Generator from typing import Generator
import re import re
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
from docx.text.paragraph import Paragraph as DocxParagraph from docx.text.paragraph import Paragraph as DocxParagraph
from docx.shared import Parented, Length from docx.shared import Parented, Length
@@ -35,7 +34,8 @@ class Heading(Paragraph):
if not numbered: if not numbered:
self._remove_numbering() self._remove_numbering()
self._docx_paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER # Alignment decided later in Renderer: only СОДЕРЖАНИЕ / СПИСОК centered.
# ВВЕДЕНИЕ / ЗАКЛЮЧЕНИЕ / ПРИЛОЖЕНИЯ stay left like Heading 1 (1.25 cm indent).
elif numbering_mode == "manual": elif numbering_mode == "manual":
# Digits already in markdown text — kill Word list numbering to avoid "1 1 …" # Digits already in markdown text — kill Word list numbering to avoid "1 1 …"
self._remove_numbering() self._remove_numbering()
+87 -14
View File
@@ -7,20 +7,32 @@ import os.path
import requests import requests
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
from docx.shared import Parented, Length from docx.shared import Parented, Length, Mm
from docx.text.paragraph import Paragraph from docx.text.paragraph import Paragraph
from .caption import Caption, CaptionInfo from .caption import Caption, CaptionInfo
from .renderable import Renderable from .renderable import Renderable
from .requires_numbering import RequiresNumbering from .requires_numbering import RequiresNumbering
from ..docx_svg import attach_svg_blip
from ..layout_tracker import LayoutState from ..layout_tracker import LayoutState
from ..rendered_info import RenderedInfo from ..rendered_info import RenderedInfo
from ..sub_renderable import SubRenderable from ..sub_renderable import SubRenderable
from ..util import create_element from ..util import create_element
# Leave room under the figure so «Рисунок N — …» stays on the same page.
_CAPTION_RESERVE = Mm(12)
class Image(Renderable, RequiresNumbering): class Image(Renderable, RequiresNumbering):
def __init__(self, parent: Parented, path: str, caption_info: CaptionInfo | None = None): def __init__(
self,
parent: Parented,
path: str,
caption_info: CaptionInfo | None = None,
svg_path: str | None = None,
*,
pixel_scale: float = 1.0,
):
super().__init__("Рисунок") super().__init__("Рисунок")
self._parent = parent self._parent = parent
self._caption_info = caption_info self._caption_info = caption_info
@@ -31,6 +43,8 @@ class Image(Renderable, RequiresNumbering):
self._docx_paragraph.paragraph_format.line_spacing = 1 self._docx_paragraph.paragraph_format.line_spacing = 1
self._docx_paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER self._docx_paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
self._invalid = False self._invalid = False
self._native_width: Length | None = None
self._native_height: Length | None = None
run = self._docx_paragraph.add_run() run = self._docx_paragraph.add_run()
@@ -47,13 +61,56 @@ class Image(Renderable, RequiresNumbering):
except FileNotFoundError: except FileNotFoundError:
logging.warning(f"Invalid image path: {path}, skipping...") logging.warning(f"Invalid image path: {path}, skipping...")
self._invalid = True self._invalid = True
self._image = None
# High-res PlantUML/Kroki PNG: keep on-page size as if scale were 1.
if not self._invalid and pixel_scale and pixel_scale > 1:
self._image.width = Length(int(self._image.width / pixel_scale))
self._image.height = Length(int(self._image.height / pixel_scale))
if not self._invalid:
self._native_width = Length(int(self._image.width))
self._native_height = Length(int(self._image.height))
if not self._invalid and svg_path:
try:
attach_svg_blip(run, self._image, svg_path)
except Exception as exc:
logging.warning("SVG blip attach failed (%s): %s", svg_path, exc)
self._number = None self._number = None
self._caption: Caption | None = None
self.landscape = bool(caption_info and caption_info.landscape)
if caption_info and caption_info.unique_name: if caption_info and caption_info.unique_name:
self.unique_name = caption_info.unique_name self.unique_name = caption_info.unique_name
# Keep figure + caption together across Word pagination.
if not self._invalid and caption_info is not None:
self._docx_paragraph.paragraph_format.keep_with_next = True
def set_number(self, number: str): def set_number(self, number: str):
self._number = number self._number = number
self._caption = None
def _get_caption(self) -> Caption:
"""One caption object per figure.
Paragraph.render() lays a figure out once to learn its height and the
Renderer then renders it for real; a shared caption keeps the second
pass from re-measuring the same line.
"""
if self._caption is None:
self._caption = Caption(
self._parent, "Рисунок", self._caption_info, self._number, False
)
self._caption.center()
return self._caption
def _reset_native_size(self) -> None:
if self._image is None or self._native_width is None or self._native_height is None:
return
self._image.width = Length(int(self._native_width))
self._image.height = Length(int(self._native_height))
def render(self, previous_rendered: RenderedInfo, layout_state: LayoutState)\ def render(self, previous_rendered: RenderedInfo, layout_state: LayoutState)\
-> Generator[RenderedInfo | SubRenderable, None, None]: -> Generator[RenderedInfo | SubRenderable, None, None]:
@@ -61,28 +118,44 @@ class Image(Renderable, RequiresNumbering):
yield from [] yield from []
return return
# Re-fit from native size each time (Paragraph may have measured us earlier).
self._reset_native_size()
has_caption = self._caption_info is not None
caption_reserve = _CAPTION_RESERVE if has_caption else Length(0)
max_w = layout_state.max_width
max_h = Length(max(0, int(layout_state.max_height) - int(caption_reserve)))
# limit width # limit width
if self._image.width > layout_state.max_width: if self._image.width > max_w:
height_by_width = self._image.height / self._image.width height_by_width = self._image.height / self._image.width
self._image.width = layout_state.max_width self._image.width = max_w
self._image.height = Length(self._image.width * height_by_width) self._image.height = Length(self._image.width * height_by_width)
# limit height # limit height (leave room for caption on the same page)
if self._image.height > layout_state.max_height: if self._image.height > max_h:
width_by_height = self._image.width / self._image.height width_by_height = self._image.width / self._image.height
self._image.height = layout_state.max_height self._image.height = max_h
self._image.width = Length(self._image.height * width_by_height) self._image.width = Length(self._image.height * width_by_height)
height = self._image.height height = self._image.height
need = Length(int(height) + int(caption_reserve))
if layout_state.remaining_page_height < height: remaining = layout_state.remaining_page_height
height += layout_state.remaining_page_height if remaining < need:
if self.landscape:
# Already on a fresh landscape section — shrink instead of soft page-break
# (soft break + section break = empty landscape page before the figure).
avail = Length(max(0, int(remaining) - int(caption_reserve)))
if avail > 0 and self._image.height > avail:
ratio = int(avail) / int(self._image.height)
self._image.height = avail
self._image.width = Length(int(self._image.width * ratio))
height = self._image.height
else:
height = Length(int(height) + int(remaining))
yield (rendered_image := RenderedInfo(self._docx_paragraph, Length(height))) yield (rendered_image := RenderedInfo(self._docx_paragraph, Length(height)))
layout_state.add_height(rendered_image.height) layout_state.add_height(rendered_image.height)
caption = Caption(self._parent, "Рисунок", self._caption_info, self._number, False) yield from self._get_caption().render(rendered_image, copy(layout_state))
caption.center()
yield from caption.render(rendered_image, copy(layout_state))

Some files were not shown because too many files have changed in this diff Show More