From 510f7e7adf07462484394cd6570214638b59a138 Mon Sep 17 00:00:00 2001 From: Igor20264 Date: Tue, 8 Sep 2026 19:37:54 +0300 Subject: [PATCH] v0.5.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Что то сделал --- .gitattributes | 2 + .gitignore | 5 + .python-version | 1 + PROJECT.md | 86 +- ReadMe.md | 6 +- _bpmn_preview.png | Bin 17970 -> 0 bytes _bpmn_sprites.puml | 3468 ------------------------- _method_extract.txt | 988 +++++++ build-exe-fast.bat | 32 +- build-exe-uvx.bat | 128 + build-exe.bat | 28 +- build-find-python311.bat | 90 + delete_this.txt | 4 + docs/README.md | 13 +- docs/archimate.md | 662 +++++ docs/cli-gui.md | 26 +- docs/markdown.md | 8 +- docs/prompts.md | 7 +- docs/quickstart.md | 5 +- docs/schemes.md | 170 +- docs/title.md | 187 ++ docs/word2md.md | 46 + md2gost.fast.spec | 33 +- md2gost.spec | 27 +- md2gost.user.json | 14 + md2gost/README.md | 40 +- md2gost/TitleTemplate.docx | Bin 0 -> 206244 bytes md2gost/__main__.py | 84 +- md2gost/biblio_processor.py | 19 +- md2gost/converter.py | 11 +- md2gost/dfd.py | 477 ++++ md2gost/diagram_renderer.py | 55 +- md2gost/diagram_schemes.py | 157 +- md2gost/diagrams/BPMN.puml | 1126 -------- md2gost/diagrams/schemes.json | 14 +- md2gost/dnd.py | 7 + md2gost/doc_metadata.py | 148 ++ md2gost/gui.py | 415 ++- md2gost/help_content.py | 103 +- md2gost/idef0.py | 2185 ++++++++++++++++ md2gost/page_geometry.py | 140 +- md2gost/pipeline.py | 161 +- md2gost/profiles.py | 2 +- md2gost/renderable/paragraph.py | 8 +- md2gost/renderable/table.py | 104 +- md2gost/renderer.py | 3 + md2gost/title_page.py | 323 +++ md2gost/user_profile.py | 60 + md2gost/word_fix.py | 411 ++- prompts/README.md | 3 +- prompts/emulate-student.md | 57 + prompts/generate-md.md | 52 +- prompts/generate-mirea-report.md | 16 +- prompts/generate-pis-custom-report.md | 2 +- pyproject.toml | 14 +- scripts/_gen_bpmn_sprites.py | 461 ---- scripts/fetch_graphviz.py | 44 + scripts/pyi_bundle.py | 111 + scripts/pyi_rth_tcltk.py | 33 + tests/_idef0_preview.png | Bin 0 -> 71059 bytes tests/_idef0_preview.svg | 78 + tests/_idef0_preview_a0.png | Bin 0 -> 116528 bytes tests/_idef0_preview_a0.svg | 166 ++ tests/test_dfd.py | 194 ++ tests/test_diagram_renderer.py | 204 +- tests/test_doc_metadata.py | 228 ++ tests/test_idef0.py | 422 +++ tests/test_landscape.py | 52 + tests/test_mirea_tz.py | 49 + tests/test_page_geometry.py | 114 +- tests/test_pipeline.py | 1 + tests/test_title_page.py | 291 +++ tests/test_word2md.py | 378 +++ tests/test_word_fix.py | 30 + todo.txt | 4 +- uv.lock | 155 +- word2md/README.md | 9 + word2md/__init__.py | 7 + word2md/__main__.py | 58 + word2md/blocks.py | 110 + word2md/captions.py | 85 + word2md/classify.py | 167 ++ word2md/emit.py | 249 ++ word2md/media.py | 91 + word2md/omml.py | 149 ++ word2md/pipeline.py | 110 + word2md/refs.py | 170 ++ word2md/tables.py | 140 + word2md/text.py | 78 + word2md/walker.py | 596 +++++ 90 files changed, 11720 insertions(+), 5547 deletions(-) create mode 100644 .gitattributes create mode 100644 .python-version delete mode 100644 _bpmn_preview.png delete mode 100644 _bpmn_sprites.puml create mode 100644 _method_extract.txt create mode 100644 build-exe-uvx.bat create mode 100644 build-find-python311.bat create mode 100644 delete_this.txt create mode 100644 docs/archimate.md create mode 100644 docs/title.md create mode 100644 docs/word2md.md create mode 100644 md2gost.user.json create mode 100644 md2gost/TitleTemplate.docx create mode 100644 md2gost/dfd.py delete mode 100644 md2gost/diagrams/BPMN.puml create mode 100644 md2gost/doc_metadata.py create mode 100644 md2gost/idef0.py create mode 100644 md2gost/title_page.py create mode 100644 md2gost/user_profile.py create mode 100644 prompts/emulate-student.md delete mode 100644 scripts/_gen_bpmn_sprites.py create mode 100644 scripts/fetch_graphviz.py create mode 100644 scripts/pyi_bundle.py create mode 100644 scripts/pyi_rth_tcltk.py create mode 100644 tests/_idef0_preview.png create mode 100644 tests/_idef0_preview.svg create mode 100644 tests/_idef0_preview_a0.png create mode 100644 tests/_idef0_preview_a0.svg create mode 100644 tests/test_dfd.py create mode 100644 tests/test_doc_metadata.py create mode 100644 tests/test_idef0.py create mode 100644 tests/test_title_page.py create mode 100644 tests/test_word2md.py create mode 100644 word2md/README.md create mode 100644 word2md/__init__.py create mode 100644 word2md/__main__.py create mode 100644 word2md/blocks.py create mode 100644 word2md/captions.py create mode 100644 word2md/classify.py create mode 100644 word2md/emit.py create mode 100644 word2md/media.py create mode 100644 word2md/omml.py create mode 100644 word2md/pipeline.py create mode 100644 word2md/refs.py create mode 100644 word2md/tables.py create mode 100644 word2md/text.py create mode 100644 word2md/walker.py diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2b65f6f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.bat text eol=crlf +*.cmd text eol=crlf diff --git a/.gitignore b/.gitignore index 5c577f9..2a4116b 100644 --- a/.gitignore +++ b/.gitignore @@ -133,6 +133,11 @@ venv.bak/ # md2gost diagram PNG 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/ diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/PROJECT.md b/PROJECT.md index 091adca..9f26420 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -14,7 +14,7 @@ - оформляет DOCX стилями Times New Roman, полями 30/10/20/20 мм, интервалом 1.5; - нумерует рисунки, таблицы, листинги, формулы; - собирает содержание и список источников; -- рисует UML / C4 / BPMN / Mermaid; +- рисует UML / C4 / Mermaid; - проверяет структуру по ТЗ (`--check`); - при необходимости режет длинные таблицы и листинги через Microsoft Word и пишет «Продолжение…». @@ -29,6 +29,7 @@ | **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]: …`. @@ -37,6 +38,7 @@ - **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`. --- @@ -46,11 +48,12 @@ 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). +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]`). @@ -126,6 +129,10 @@ Windows exe: `build-exe.bat` → `dist\md2gost.exe` (двойной клик — | `--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 в листингах | @@ -150,6 +157,8 @@ Windowed exe: `_argv_needs_console()` + `_enable_windows_console()` — конс - зона 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/` + кнопки схем); @@ -170,7 +179,7 @@ Windowed exe: `_argv_needs_console()` + `_enable_windows_console()` — конс | Таблица | `%id Подпись` перед таблицей | подпись «Таблица N — …» | | Merge ячеек | `^` rowspan, `>` colspan | OOXML `vMerge` / `gridSpan`; через разрыв страницы merge не переносится | | Листинг | `%id` + code fence | Courier New, опционально Pygments | -| Диаграмма | `%id` + ` ```uml ` / `uml-c4` / `bpmn` / `mermaid` | Рисунок; флаги `+listing`, `+landscape` | +| Диаграмма | `%id` + ` ```uml ` / `uml-c4` / `mermaid` / `idef0` / `dfd` | Рисунок; флаги `+listing`, `+landscape` | | Формула | `%eq1` + `$$…$$` + `@Формула:eq1` | номер только если есть ссылка | | Инлайн-формула | `$…$` | OMML в абзаце | | Ссылка на объект | `@Рисунок:id`, `@Таблица:id`, `@Листинг:id`, `@Формула:id` | подстановка номера | @@ -187,7 +196,7 @@ Windowed exe: `_argv_needs_console()` + `_enable_windows_console()` — конс ### 5.5. Схемы и диаграммы -Встроенные id (`md2gost/diagrams/schemes.json`): `c4`, `c4context`, `c4component`, `usecase`, `bpmn`. +Встроенные id (`md2gost/diagrams/schemes.json`): `c4`, `c4context`, `c4component`, `usecase`. Слои загрузки (позже побеждает): встроенный шаблон → `md2gost.schemes.json` рядом с приложением (создаётся при первом запуске, не перезаписывается) → рядом с `.md` → `--schemes`. @@ -197,7 +206,7 @@ Windowed exe: `_argv_needs_console()` + `_enable_windows_console()` — конс 2. локальный Kroki (`KROKI_URL` / `--kroki-url`); 3. `https://kroki.io` при `--diagram-fallback remote`. -Mermaid — только Kroki. **IDEF0 / DFD конвертер не рисует** — только готовый PNG. +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/`. @@ -252,6 +261,10 @@ DOCX сам не умеет писать «Продолжение Таблицы Если в документе нет 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`. @@ -265,14 +278,16 @@ DOCX сам не умеет писать «Продолжение Таблицы | `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. +- `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 для той же эвристики вёрстки. @@ -304,10 +319,21 @@ DOCX сам не умеет писать «Продолжение Таблицы - `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`, не пробрасывает исключение наружу. +- `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, применяет стили, парсит, нумерует, рендерит. @@ -481,8 +507,13 @@ DOCX сам не умеет писать «Продолжение Таблицы - `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/diagrams/`** — `schemes.json`, `BPMN.puml`, C4 `*.puml`. +**`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 и вёрстка @@ -514,7 +545,7 @@ DOCX сам не умеет писать «Продолжение Таблицы **`md2gost/gui.py`** - `_make_root`, `_combo_values/_combo_key/_display`, `_Tooltip`, `_tip`. -- `Md2GostApp`: меню, окна файлов/диаграмм/схем/справки, браузер docs, сборка промпта, DnD, `_collect()` → `ConvertRequest`, `_convert`. +- `Md2GostApp`: меню, окна файлов/студента/метаданных/диаграмм/схем/справки, браузер docs, сборка промпта, DnD, `_collect()` → `ConvertRequest`, `_convert`. - `run_gui(initial)`, `main()`. **`md2gost/dnd.py`** @@ -561,6 +592,27 @@ DOCX сам не умеет писать «Продолжение Таблицы --- +## 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`. @@ -575,9 +627,12 @@ DOCX сам не умеет писать «Продолжение Таблицы | `test_landscape.py` | геометрия альбома, секции, картинки/таблицы/листинги, один section break | | `test_layout_tracker.py` | страницы и остаток высоты | | `test_paragraph.py` / `test_paragraph_sizer.py` | абзацы и измерение шрифта | -| `test_diagram_renderer.py` | схемы, BPMN, кэш, mermaid, svg, jar, `+listing`/`+landscape` | +| `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_mardown/formula.py` | формулы (unittest) | Нет полного e2e «открыть Word и сравнить каждую страницу» в Linux CI (шрифты ставятся, Word COM — нет). @@ -594,11 +649,12 @@ DOCX сам не умеет писать «Продолжение Таблицы | `docs/README.md` | оглавление docs | | `docs/quickstart.md` | установка, exe, типы | | `docs/markdown.md` | диалект | -| `docs/schemes.md` | UML/Mermaid/BPMN, JSON схем | +| `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` | образцы | @@ -612,7 +668,7 @@ DOCX сам не умеет писать «Продолжение Таблицы Опционально: pywin32 (Word COM), matplotlib (сборка exe / отладка), tkinterdnd2 (удобный DnD в GUI). -Внешние рантаймы: Java + plantuml.jar **или** Kroki; для PDF — XeLaTeX / latexmk и шрифт Times New Roman. +Внешние рантаймы: Java + plantuml.jar **или** Kroki; Graphviz (вшитый/кэш/PATH) для DFD; для PDF — XeLaTeX / latexmk и шрифт Times New Roman. --- @@ -626,13 +682,13 @@ DOCX сам не умеет писать «Продолжение Таблицы - `Other_Code/` — справка OOXML, docx-js, скрипты pack/unpack/validate (не импортируется md2gost); - артефакты сборки `build-fast/`, кэши диаграмм. -`todo.txt` сейчас: «ожидаем BPMN в mermaid» и «оценки с текстом через сам Word, а не предугадывание» — второе частично закрыто режимом `word`. +`todo.txt` сейчас: «оценки с текстом через сам Word, а не предугадывание» — частично закрыто режимом `word`. --- ## 13. Чего нет / ограничения (честно) -- IDEF0 и DFD конвертер не рисует. +- DFD: пакет `data-flow-diagram` в зависимостях; картинка — вшитый/кэш/PATH Graphviz или Kroki; балансировка уровней не проверяется. - FODT и LaTeX не догоняют md2gost по GUI, checker, схемам, профилям PIS/АПИД, Word COM. - «Продолжение таблицы» в чистом DOCX без Word невозможно точно (нет longtable). Без Word — `off` или неточный `legacy`/`caption`. - `--check-pages` даёт ложные срабатывания; это эвристика. @@ -649,7 +705,7 @@ DOCX сам не умеет писать «Продолжение Таблицы 2. **ВКР:** раздельный список, ссылки `[1.5]`, приложение «Графический материал». 3. **ПИС:** `# Практическая работа №1. …` → `--type PIS_custom`, сквозные Рисунок 1, 2, 3. 4. **АПИД:** обязательные 2 главы и 2.1–2.4, источники 7–20. -5. **UML в отчёте:** `%id Подпись` + ` ```uml-c4 ` / ` ```bpmn ` — PNG/SVG в Word, опционально `+listing` и `+landscape`. +5. **UML в отчёте:** `%id Подпись` + ` ```uml-c4 ` — PNG/SVG в Word, опционально `+listing` и `+landscape`. 6. **Широкая таблица:** `%id … +landscape`. 7. **Длинная таблица на Windows:** режим `word` → COM режет и пишет «Продолжение Таблицы N». 8. **Свой вуз/кафедра:** `md2gost.styles.json` меняет поля и H1 без правки кода. diff --git a/ReadMe.md b/ReadMe.md index 5fade5e..286073d 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1,6 +1,6 @@ # GhostEditor / md2gost -Конвертер **Markdown → DOCX** по методическим указаниям РТУ МИРЭА (ГОСТ 7.32-2017) и профилям ПИС / АПИД. Есть GUI, CLI и промпты для генерации `.md` через ИИ. +Конвертер **Markdown → DOCX** по методическим указаниям РТУ МИРЭА (ГОСТ 7.32-2017) и профилям ПИС / АПИД. Есть GUI, CLI, импорт **Word → Markdown** (`word2md`) и промпты для генерации `.md` через ИИ. ## Быстрый старт @@ -8,6 +8,7 @@ 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 ``` Windows: `build-exe.bat` → `dist\md2gost.exe` (двойной клик — GUI). @@ -22,9 +23,10 @@ Windows: `build-exe.bat` → `dist\md2gost.exe` (двойной клик — GUI |--------|------------| | [Быстрый старт](docs/quickstart.md) | Установка, GUI, типы документов | | [Синтаксис Markdown](docs/markdown.md) | Диалект md2gost vs обычный MD | -| [Схемы и диаграммы](docs/schemes.md) | PlantUML, Mermaid, `md2gost.schemes.json` | +| [Схемы и диаграммы](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). diff --git a/_bpmn_preview.png b/_bpmn_preview.png deleted file mode 100644 index 05cf39e08f722e0ffae8961963e2bcd2a42901f7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17970 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 BB3J+b diff --git a/_bpmn_sprites.puml b/_bpmn_sprites.puml deleted file mode 100644 index 541241d..0000000 --- a/_bpmn_sprites.puml +++ /dev/null @@ -1,3468 +0,0 @@ -' auto-generated geometric sprites (BPMN 2.0 notation) -sprite $evStart [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000012345432100000000000000000000 -0000000000000000259BCCCCCCCB95200000000000000000 -0000000000000027CC95322122359CC72000000000000000 -00000000000016CB520000000000025BC610000000000000 -000000000003AC6100000000000000016CA3000000000000 -00000000004BA300000000000000000003AB400000000000 -0000000004B8100000000000000000000018B40000000000 -000000003B810000000000000000000000018B3000000000 -00000001AA100000000000000000000000001AA100000000 -00000006C30000000000000000000000000003C600000000 -0000002C6000000000000000000000000000006C20000000 -0000007B1000000000000000000000000000001B70000000 -000002C500000000000000000000000000000005C2000000 -000005C200000000000000000000000000000002C5000000 -000009900000000000000000000000000000000099000000 -00001B50000000000000000000000000000000005B100000 -00002C30000000000000000000000000000000003C200000 -00003C20000000000000000000000000000000002C300000 -00004C20000000000000000000000000000000002C400000 -00005C10000000000000000000000000000000001C500000 -00004C20000000000000000000000000000000002C400000 -00003C20000000000000000000000000000000002C300000 -00002C30000000000000000000000000000000003C200000 -00001B50000000000000000000000000000000005B100000 -000009900000000000000000000000000000000099000000 -000005C200000000000000000000000000000002C5000000 -000002C500000000000000000000000000000005C2000000 -0000007B1000000000000000000000000000001B70000000 -0000002C6000000000000000000000000000006C20000000 -00000006C30000000000000000000000000003C600000000 -00000001AA100000000000000000000000001AA100000000 -000000003B810000000000000000000000018B3000000000 -0000000004B8100000000000000000000018B40000000000 -00000000004BA300000000000000000003AB400000000000 -000000000003AC6100000000000000016CA3000000000000 -00000000000016CB520000000000025BC610000000000000 -0000000000000027CC95322122359CC72000000000000000 -0000000000000000259BCCCCCCCB95200000000000000000 -000000000000000000012345432100000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evStartMsg [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000012345432100000000000000000000 -0000000000000000259BCCCCCCCB95200000000000000000 -0000000000000027CC95322122359CC72000000000000000 -00000000000016CB520000000000025BC610000000000000 -000000000003AC6100000000000000016CA3000000000000 -00000000004BA300000000000000000003AB400000000000 -0000000004B8100000000000000000000018B40000000000 -000000003B810000000000000000000000018B3000000000 -00000001AA100000000000000000000000001AA100000000 -00000006C30000000000000000000000000003C600000000 -0000002C6000000000000000000000000000006C20000000 -0000007B1000000000000000000000000000001B70000000 -000002C500000000000000000000000000000005C2000000 -000005C200000008888888888888888800000002C5000000 -000009900000008FFFFFFFFFFFFFFFFF8000000099000000 -00001B500000008FDA88888888888ADF800000005B100000 -00002C300000008F854000000000458F800000003C200000 -00003C200000008F815510000015518F800000002C300000 -00004C200000008F800451000154008F800000002C400000 -00005C100000008F800036202630008F800000001C500000 -00004C200000008F800002646200008F800000002C400000 -00003C200000008F800000252000008F800000002C300000 -00002C300000008F800000000000008F800000003C200000 -00001B500000008FB8888888888888BF800000005B100000 -000009900000008FFFFFFFFFFFFFFFFF8000000099000000 -000005C200000008888888888888888800000002C5000000 -000002C500000000000000000000000000000005C2000000 -0000007B1000000000000000000000000000001B70000000 -0000002C6000000000000000000000000000006C20000000 -00000006C30000000000000000000000000003C600000000 -00000001AA100000000000000000000000001AA100000000 -000000003B810000000000000000000000018B3000000000 -0000000004B8100000000000000000000018B40000000000 -00000000004BA300000000000000000003AB400000000000 -000000000003AC6100000000000000016CA3000000000000 -00000000000016CB520000000000025BC610000000000000 -0000000000000027CC95322122359CC72000000000000000 -0000000000000000259BCCCCCCCB95200000000000000000 -000000000000000000012345432100000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evStartTimer [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000012345432100000000000000000000 -0000000000000000259BCCCCCCCB95200000000000000000 -0000000000000027CC95322122359CC72000000000000000 -00000000000016CB520000000000025BC610000000000000 -000000000003AC6100000000000000016CA3000000000000 -00000000004BA300000000000000000003AB400000000000 -0000000004B8100000000000000000000018B40000000000 -000000003B810000000000000000000000018B3000000000 -00000001AA100000000000000000000000001AA100000000 -00000006C30000000000000000000000000003C600000000 -0000002C6000000000000000000000000000006C20000000 -0000007B1000000000000133310000000000001B70000000 -000002C50000000000039BBBBB93000000000005C2000000 -000005C200000000006C8312138C600000000002C5000000 -000009900000000006B400151004B6000000000099000000 -00001B50000000003C40001510004C30000000005B100000 -00002C30000000009800001510000890000000003C200000 -00003C2000000001B3000015100003B1000000002C300000 -00004C2000000003B1000015111001B3000000002C400000 -00005C1000000003B0000015555300B3000000001C500000 -00004C2000000003B1000001111001B3000000002C400000 -00003C2000000001B3000000000003B1000000002C300000 -00002C30000000009800000000000890000000003C200000 -00001B50000000003C40000000004C30000000005B100000 -000009900000000006B400000004B6000000000099000000 -000005C200000000006C8310138C600000000002C5000000 -000002C50000000000039BBBBB93000000000005C2000000 -0000007B1000000000000133310000000000001B70000000 -0000002C6000000000000000000000000000006C20000000 -00000006C30000000000000000000000000003C600000000 -00000001AA100000000000000000000000001AA100000000 -000000003B810000000000000000000000018B3000000000 -0000000004B8100000000000000000000018B40000000000 -00000000004BA300000000000000000003AB400000000000 -000000000003AC6100000000000000016CA3000000000000 -00000000000016CB520000000000025BC610000000000000 -0000000000000027CC95322122359CC72000000000000000 -0000000000000000259BCCCCCCCB95200000000000000000 -000000000000000000012345432100000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evStartSignal [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000012345432100000000000000000000 -0000000000000000259BCCCCCCCB95200000000000000000 -0000000000000027CC95322122359CC72000000000000000 -00000000000016CB520000000000025BC610000000000000 -000000000003AC6100000000000000016CA3000000000000 -00000000004BA300000000000000000003AB400000000000 -0000000004B8100000000000000000000018B40000000000 -000000003B810000000000000000000000018B3000000000 -00000001AA100000000000000000000000001AA100000000 -00000006C30000000000000000000000000003C600000000 -0000002C6000000000000000000000000000006C20000000 -0000007B1000000000000003000000000000001B70000000 -000002C500000000000000373000000000000005C2000000 -000005C200000000000001646100000000000002C5000000 -000009900000000000000351530000000000000099000000 -00001B50000000000000162026100000000000005B100000 -00002C30000000000000450005400000000000003C200000 -00003C20000000000001620002610000000000002C300000 -00004C20000000000004500000540000000000002C400000 -00005C10000000000026200000262000000000001C500000 -00004C20000000000054000000045000000000002C400000 -00003C20000000000261000000016200000000002C300000 -00002C30000000000540000000004500000000003C200000 -00001B50000000002610000000001620000000005B100000 -000009900000000054111111111114500000000099000000 -000005C200000002666666666666666200000002C5000000 -000002C500000000111111111111111000000005C2000000 -0000007B1000000000000000000000000000001B70000000 -0000002C6000000000000000000000000000006C20000000 -00000006C30000000000000000000000000003C600000000 -00000001AA100000000000000000000000001AA100000000 -000000003B810000000000000000000000018B3000000000 -0000000004B8100000000000000000000018B40000000000 -00000000004BA300000000000000000003AB400000000000 -000000000003AC6100000000000000016CA3000000000000 -00000000000016CB520000000000025BC610000000000000 -0000000000000027CC95322122359CC72000000000000000 -0000000000000000259BCCCCCCCB95200000000000000000 -000000000000000000012345432100000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evStartCond [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000012345432100000000000000000000 -0000000000000000259BCCCCCCCB95200000000000000000 -0000000000000027CC95322122359CC72000000000000000 -00000000000016CB520000000000025BC610000000000000 -000000000003AC6100000000000000016CA3000000000000 -00000000004BA300000000000000000003AB400000000000 -0000000004B8100000000000000000000018B40000000000 -000000003B810000000000000000000000018B3000000000 -00000001AA100000000000000000000000001AA100000000 -00000006C30000000000000000000000000003C600000000 -0000002C6000000004888888888884000000006C20000000 -0000007B100000004BFFFFFFFFFFFB400000001B70000000 -000002C5000000008FB888888888BF8000000005C2000000 -000005C2000000008F80000000008F8000000002C5000000 -00000990000000008F80000000008F800000000099000000 -00001B50000000008F80000000008F80000000005B100000 -00002C30000000008F82555555528F80000000003C200000 -00003C20000000008F80000000008F80000000002C300000 -00004C20000000008F80000000008F80000000002C400000 -00005C10000000008F82555555528F80000000001C500000 -00004C20000000008F80000000008F80000000002C400000 -00003C20000000008F80000000008F80000000002C300000 -00002C30000000008F82555555528F80000000003C200000 -00001B50000000008F80000000008F80000000005B100000 -00000990000000008F80000000008F800000000099000000 -000005C2000000008F80000000008F8000000002C5000000 -000002C5000000008FB888888888BF8000000005C2000000 -0000007B100000004BFFFFFFFFFFFB400000001B70000000 -0000002C6000000004888888888884000000006C20000000 -00000006C30000000000000000000000000003C600000000 -00000001AA100000000000000000000000001AA100000000 -000000003B810000000000000000000000018B3000000000 -0000000004B8100000000000000000000018B40000000000 -00000000004BA300000000000000000003AB400000000000 -000000000003AC6100000000000000016CA3000000000000 -00000000000016CB520000000000025BC610000000000000 -0000000000000027CC95322122359CC72000000000000000 -0000000000000000259BCCCCCCCB95200000000000000000 -000000000000000000012345432100000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evStartError [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000012345432100000000000000000000 -0000000000000000259BCCCCCCCB95200000000000000000 -0000000000000027CC95322122359CC72000000000000000 -00000000000016CB520000000000025BC610000000000000 -000000000003AC6100000000000000016CA3000000000000 -00000000004BA300000000000000000003AB400000000000 -0000000004B8100000000000000000000018B40000000000 -000000003B810000000000000000000000018B3000000000 -00000001AA100000000000000000000000001AA100000000 -00000006C30000000000000000000000000003C600000000 -0000002C6000000000000000100000000000006C20000000 -0000007B1000000000000001510000000000001B70000000 -000002C500000000000000045000000000000005C2000000 -000005C200000000000000162000000000000002C5000000 -000009900000000000000045000000000000000099000000 -00001B50000000000000016300000000000000005B100000 -00002C30000000000000036100000000000000003C200000 -00003C20000000000000164111100000000000002C300000 -00004C20000000000000366666610000000000002C400000 -00005C10000000000000621114400000000000001C500000 -00004C20000000000002644426200000000000002C400000 -00003C20000000000002444555000000000000002C300000 -00002C30000000000000000662000000000000003C200000 -00001B50000000000000001660000000000000005B100000 -000009900000000000000027300000000000000099000000 -000005C200000000000000461000000000000002C5000000 -000002C500000000000000640000000000000005C2000000 -0000007B1000000000000151000000000000001B70000000 -0000002C6000000000000010000000000000006C20000000 -00000006C30000000000000000000000000003C600000000 -00000001AA100000000000000000000000001AA100000000 -000000003B810000000000000000000000018B3000000000 -0000000004B8100000000000000000000018B40000000000 -00000000004BA300000000000000000003AB400000000000 -000000000003AC6100000000000000016CA3000000000000 -00000000000016CB520000000000025BC610000000000000 -0000000000000027CC95322122359CC72000000000000000 -0000000000000000259BCCCCCCCB95200000000000000000 -000000000000000000012345432100000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evStartEsc [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000012345432100000000000000000000 -0000000000000000259BCCCCCCCB95200000000000000000 -0000000000000027CC95322122359CC72000000000000000 -00000000000016CB520000000000025BC610000000000000 -000000000003AC6100000000000000016CA3000000000000 -00000000004BA300000000000000000003AB400000000000 -0000000004B8100000000000000000000018B40000000000 -000000003B810000000000000000000000018B3000000000 -00000001AA100000000000000000000000001AA100000000 -00000006C30000000000000000000000000003C600000000 -0000002C6000000000000000000000000000006C20000000 -0000007B1000000000000000000000000000001B70000000 -000002C500000000000000000000000000000005C2000000 -000005C200000000000000000000000000000002C5000000 -000009900000000000000000000000000000000099000000 -00001B50000000000000000000000000000000005B100000 -00002C30000000000000000100000000000000003C200000 -00003C20000000000000002620000000000000002C300000 -00004C20000000000000026662000000000000002C400000 -00005C10000000000000263636200000000000001C500000 -00004C20000000000002621612620000000000002C400000 -00003C20000000000026201610262000000000002C300000 -00002C30000000000263112621136200000000003C200000 -00001B50000000001666666666666610000000005B100000 -000009900000000001111126211111000000000099000000 -000005C200000000000000161000000000000002C5000000 -000002C500000000000000151000000000000005C2000000 -0000007B1000000000000001000000000000001B70000000 -0000002C6000000000000000000000000000006C20000000 -00000006C30000000000000000000000000003C600000000 -00000001AA100000000000000000000000001AA100000000 -000000003B810000000000000000000000018B3000000000 -0000000004B8100000000000000000000018B40000000000 -00000000004BA300000000000000000003AB400000000000 -000000000003AC6100000000000000016CA3000000000000 -00000000000016CB520000000000025BC610000000000000 -0000000000000027CC95322122359CC72000000000000000 -0000000000000000259BCCCCCCCB95200000000000000000 -000000000000000000012345432100000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evStartMulti [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000012345432100000000000000000000 -0000000000000000259BCCCCCCCB95200000000000000000 -0000000000000027CC95322122359CC72000000000000000 -00000000000016CB520000000000025BC610000000000000 -000000000003AC6100000000000000016CA3000000000000 -00000000004BA300000000000000000003AB400000000000 -0000000004B8100000000000000000000018B40000000000 -000000003B810000000000000000000000018B3000000000 -00000001AA100000000000000000000000001AA100000000 -00000006C30000000000000000000000000003C600000000 -0000002C6000000000000000000000000000006C20000000 -0000007B1000000000000013100000000000001B70000000 -000002C500000000000001565100000000000005C2000000 -000005C200000000000036313630000000000002C5000000 -000009900000000000146200026410000000000099000000 -00001B50000000000255100000155200000000005B100000 -00002C30000000003630000000003630000000003C200000 -00003C20000000016200000000000261000000002C300000 -00004C20000000005300000000000350000000002C400000 -00005C10000000002500000000000520000000001C500000 -00004C20000000001610000000001610000000002C400000 -00003C20000000000530000000003500000000002C300000 -00002C30000000000350000000005300000000003C200000 -00001B50000000000161000000016100000000005B100000 -000009900000000000531111111350000000000099000000 -000005C200000000003666666666300000000002C5000000 -000002C500000000000111111111000000000005C2000000 -0000007B1000000000000000000000000000001B70000000 -0000002C6000000000000000000000000000006C20000000 -00000006C30000000000000000000000000003C600000000 -00000001AA100000000000000000000000001AA100000000 -000000003B810000000000000000000000018B3000000000 -0000000004B8100000000000000000000018B40000000000 -00000000004BA300000000000000000003AB400000000000 -000000000003AC6100000000000000016CA3000000000000 -00000000000016CB520000000000025BC610000000000000 -0000000000000027CC95322122359CC72000000000000000 -0000000000000000259BCCCCCCCB95200000000000000000 -000000000000000000012345432100000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evStartPar [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000012345432100000000000000000000 -0000000000000000259BCCCCCCCB95200000000000000000 -0000000000000027CC95322122359CC72000000000000000 -00000000000016CB520000000000025BC610000000000000 -000000000003AC6100000000000000016CA3000000000000 -00000000004BA300000000000000000003AB400000000000 -0000000004B8100000000000000000000018B40000000000 -000000003B810000000000000000000000018B3000000000 -00000001AA100000000000000000000000001AA100000000 -00000006C30000000000000000000000000003C600000000 -0000002C6000000000000000000000000000006C20000000 -0000007B1000000000001356531000000000001B70000000 -000002C5000000000015BBA9ABB5100000000005C2000000 -000005C200000000019B5101015B910000000002C5000000 -000009900000000019A200151002A9100000000099000000 -00001B50000000005B20001610002B50000000005B100000 -00002C3000000001B5000016100005B1000000003C200000 -00003C2000000003B1000016100001B3000000002C300000 -00004C2000000005A0111126211110A5000000002C400000 -00005C10000000069156666666665196000000001C500000 -00004C2000000005A0111126211110A5000000002C400000 -00003C2000000003B1000016100001B3000000002C300000 -00002C3000000001B5000016100005B1000000003C200000 -00001B50000000005B20001610002B50000000005B100000 -000009900000000019A200151002A9100000000099000000 -000005C200000000019B5101015B910000000002C5000000 -000002C5000000000015BBA9ABB5100000000005C2000000 -0000007B1000000000001356531000000000001B70000000 -0000002C6000000000000000000000000000006C20000000 -00000006C30000000000000000000000000003C600000000 -00000001AA100000000000000000000000001AA100000000 -000000003B810000000000000000000000018B3000000000 -0000000004B8100000000000000000000018B40000000000 -00000000004BA300000000000000000003AB400000000000 -000000000003AC6100000000000000016CA3000000000000 -00000000000016CB520000000000025BC610000000000000 -0000000000000027CC95322122359CC72000000000000000 -0000000000000000259BCCCCCCCB95200000000000000000 -000000000000000000012345432100000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evStartNI [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000002345432000000000000000000000 -000000000000000000116CCCCC6110000000000000000000 -000000000000000111101221221011110000000000000000 -000000000000164100000000000000014610000000000000 -000000000003AC4000000000000000004CA3000000000000 -00000000004BA300000000000000000003AB400000000000 -0000000004B8100000000000000000000018B40000000000 -000000000881000000000000000000000001880000000000 -000000001110000000000000000000000000111000000000 -000000011000000000000000000000000000001100000000 -000000010000000000000000000000000000000100000000 -000000110000000000000000000000000000000110000000 -000000100000000000000000000000000000000010000000 -000005910000000000000000000000000000000195000000 -000009900000000000000000000000000000000099000000 -00001B50000000000000000000000000000000005B100000 -00002C30000000000000000000000000000000003C200000 -00003C20000000000000000000000000000000002C300000 -00004C20000000000000000000000000000000002C400000 -000026100000000000000000000000000000000016200000 -000001000000000000000000000000000000000001000000 -000001000000000000000000000000000000000001000000 -000001000000000000000000000000000000000001000000 -000001000000000000000000000000000000000001000000 -000001100000000000000000000000000000000011000000 -000000310000000000000000000000000000000130000000 -000002C500000000000000000000000000000005C2000000 -0000007B1000000000000000000000000000001B70000000 -0000002C6000000000000000000000000000006C20000000 -00000006C30000000000000000000000000003C600000000 -00000001AA000000000000000000000000000AA100000000 -000000003410000000000000000000000000143000000000 -000000000011000000000000000000000001100000000000 -000000000001100000000000000000000011000000000000 -000000000000112100000000000000012110000000000000 -000000000000018B520000000000025B8100000000000000 -0000000000000027CC95200000259CC72000000000000000 -0000000000000000259B6111116B95200000000000000000 -000000000000000000010000000100000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evStartMsgNI [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000002345432000000000000000000000 -000000000000000000116CCCCC6110000000000000000000 -000000000000000111101221221011110000000000000000 -000000000000164100000000000000014610000000000000 -000000000003AC4000000000000000004CA3000000000000 -00000000004BA300000000000000000003AB400000000000 -0000000004B8100000000000000000000018B40000000000 -000000000881000000000000000000000001880000000000 -000000001110000000000000000000000000111000000000 -000000011000000000000000000000000000001100000000 -000000010000000000000000000000000000000100000000 -000000110000000000000000000000000000000110000000 -000000100000000000000000000000000000000010000000 -000005910000000888888888888888880000000195000000 -000009900000008FFFFFFFFFFFFFFFFF8000000099000000 -00001B500000008FDA88888888888ADF800000005B100000 -00002C300000008F854000000000458F800000003C200000 -00003C200000008F815510000015518F800000002C300000 -00004C200000008F800451000154008F800000002C400000 -000026100000008F800036202630008F8000000016200000 -000001000000008F800002646200008F8000000001000000 -000001000000008F800000252000008F8000000001000000 -000001000000008F800000000000008F8000000001000000 -000001000000008FB8888888888888BF8000000001000000 -000001100000008FFFFFFFFFFFFFFFFF8000000011000000 -000000310000000888888888888888880000000130000000 -000002C500000000000000000000000000000005C2000000 -0000007B1000000000000000000000000000001B70000000 -0000002C6000000000000000000000000000006C20000000 -00000006C30000000000000000000000000003C600000000 -00000001AA000000000000000000000000000AA100000000 -000000003410000000000000000000000000143000000000 -000000000011000000000000000000000001100000000000 -000000000001100000000000000000000011000000000000 -000000000000112100000000000000012110000000000000 -000000000000018B520000000000025B8100000000000000 -0000000000000027CC95200000259CC72000000000000000 -0000000000000000259B6111116B95200000000000000000 -000000000000000000010000000100000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evStartTimerNI [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000002345432000000000000000000000 -000000000000000000116CCCCC6110000000000000000000 -000000000000000111101221221011110000000000000000 -000000000000164100000000000000014610000000000000 -000000000003AC4000000000000000004CA3000000000000 -00000000004BA300000000000000000003AB400000000000 -0000000004B8100000000000000000000018B40000000000 -000000000881000000000000000000000001880000000000 -000000001110000000000000000000000000111000000000 -000000011000000000000000000000000000001100000000 -000000010000000000000000000000000000000100000000 -000000110000000000000133310000000000000110000000 -000000100000000000039BBBBB9300000000000010000000 -0000059100000000006C8312138C60000000000195000000 -000009900000000006B400151004B6000000000099000000 -00001B50000000003C40001510004C30000000005B100000 -00002C30000000009800001510000890000000003C200000 -00003C2000000001B3000015100003B1000000002C300000 -00004C2000000003B1000015111001B3000000002C400000 -0000261000000003B0000015555300B30000000016200000 -0000010000000003B1000001111001B30000000001000000 -0000010000000001B3000000000003B10000000001000000 -000001000000000098000000000008900000000001000000 -00000100000000003C40000000004C300000000001000000 -000001100000000006B400000004B6000000000011000000 -0000003100000000006C8310138C60000000000130000000 -000002C50000000000039BBBBB93000000000005C2000000 -0000007B1000000000000133310000000000001B70000000 -0000002C6000000000000000000000000000006C20000000 -00000006C30000000000000000000000000003C600000000 -00000001AA000000000000000000000000000AA100000000 -000000003410000000000000000000000000143000000000 -000000000011000000000000000000000001100000000000 -000000000001100000000000000000000011000000000000 -000000000000112100000000000000012110000000000000 -000000000000018B520000000000025B8100000000000000 -0000000000000027CC95200000259CC72000000000000000 -0000000000000000259B6111116B95200000000000000000 -000000000000000000010000000100000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evStartSignalNI [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000002345432000000000000000000000 -000000000000000000116CCCCC6110000000000000000000 -000000000000000111101221221011110000000000000000 -000000000000164100000000000000014610000000000000 -000000000003AC4000000000000000004CA3000000000000 -00000000004BA300000000000000000003AB400000000000 -0000000004B8100000000000000000000018B40000000000 -000000000881000000000000000000000001880000000000 -000000001110000000000000000000000000111000000000 -000000011000000000000000000000000000001100000000 -000000010000000000000000000000000000000100000000 -000000110000000000000003000000000000000110000000 -000000100000000000000037300000000000000010000000 -000005910000000000000164610000000000000195000000 -000009900000000000000351530000000000000099000000 -00001B50000000000000162026100000000000005B100000 -00002C30000000000000450005400000000000003C200000 -00003C20000000000001620002610000000000002C300000 -00004C20000000000004500000540000000000002C400000 -000026100000000000262000002620000000000016200000 -000001000000000000540000000450000000000001000000 -000001000000000002610000000162000000000001000000 -000001000000000005400000000045000000000001000000 -000001000000000026100000000016200000000001000000 -000001100000000054111111111114500000000011000000 -000000310000000266666666666666620000000130000000 -000002C500000000111111111111111000000005C2000000 -0000007B1000000000000000000000000000001B70000000 -0000002C6000000000000000000000000000006C20000000 -00000006C30000000000000000000000000003C600000000 -00000001AA000000000000000000000000000AA100000000 -000000003410000000000000000000000000143000000000 -000000000011000000000000000000000001100000000000 -000000000001100000000000000000000011000000000000 -000000000000112100000000000000012110000000000000 -000000000000018B520000000000025B8100000000000000 -0000000000000027CC95200000259CC72000000000000000 -0000000000000000259B6111116B95200000000000000000 -000000000000000000010000000100000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evInter [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -0000000000000039CA53100000135AC93000000000000000 -00000000000028C82000000000000028C820000000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000005C710026BCB87678BCB620017C500000000000 -0000000005C50015BB52000000025BB51005C50000000000 -000000004C50029B510000000000015B92005C4000000000 -00000002B7002B9200000000000000029B2007B200000000 -00000008A102B810000000000000000018B201A800000000 -0000003C3019910000000000000000000199103C30000000 -00000098005B20000000000000000000002B500890000000 -000003C202B5000000000000000000000005B202C3000000 -000007A006B1000000000000000000000001B600A7000000 -00000B501B500000000000000000000000005B105B000000 -00002C303C200000000000000000000000002C303C200000 -00003B105B000000000000000000000000000B501B300000 -00005B00780000000000000000000000000008700B500000 -00006A00970000000000000000000000000007900A600000 -000069009600000000000000000000000000069009600000 -00006A00970000000000000000000000000007900A600000 -00005B00780000000000000000000000000008700B500000 -00003B105B000000000000000000000000000B501B300000 -00002C303C200000000000000000000000002C303C200000 -00000B501B500000000000000000000000005B105B000000 -000007A006B1000000000000000000000001B600A7000000 -000003C202B5000000000000000000000005B202C3000000 -00000098005B20000000000000000000002B500890000000 -0000003C3019910000000000000000000199103C30000000 -00000008A102B810000000000000000018B201A800000000 -00000002B7002B9200000000000000029B2007B200000000 -000000004C50029B510000000000015B92005C4000000000 -0000000005C50015BB52000000025BB51005C50000000000 -00000000005C710026BCB87678BCB620017C500000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000000028C82000000000000028C820000000000000 -0000000000000039CA53100000135AC93000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evCatchMsg [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -0000000000000039CA53100000135AC93000000000000000 -00000000000028C82000000000000028C820000000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000005C710026BCB87678BCB620017C500000000000 -0000000005C50015BB52000000025BB51005C50000000000 -000000004C50029B510000000000015B92005C4000000000 -00000002B7002B9200000000000000029B2007B200000000 -00000008A102B810000000000000000018B201A800000000 -0000003C3019910000000000000000000199103C30000000 -00000098005B20000000000000000000002B500890000000 -000003C202B5000000000000000000000005B202C3000000 -000007A006B1000888888888888888880001B600A7000000 -00000B501B50008FFFFFFFFFFFFFFFFF80005B105B000000 -00002C303C20008FDA88888888888ADF80002C303C200000 -00003B105B00008F854000000000458F80000B501B300000 -00005B007800008F815510000015518F800008700B500000 -00006A009700008F800451000154008F800007900A600000 -000069009600008F800036202630008F8000069009600000 -00006A009700008F800002646200008F800007900A600000 -00005B007800008F800000252000008F800008700B500000 -00003B105B00008F800000000000008F80000B501B300000 -00002C303C20008FB8888888888888BF80002C303C200000 -00000B501B50008FFFFFFFFFFFFFFFFF80005B105B000000 -000007A006B1000888888888888888880001B600A7000000 -000003C202B5000000000000000000000005B202C3000000 -00000098005B20000000000000000000002B500890000000 -0000003C3019910000000000000000000199103C30000000 -00000008A102B810000000000000000018B201A800000000 -00000002B7002B9200000000000000029B2007B200000000 -000000004C50029B510000000000015B92005C4000000000 -0000000005C50015BB52000000025BB51005C50000000000 -00000000005C710026BCB87678BCB620017C500000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000000028C82000000000000028C820000000000000 -0000000000000039CA53100000135AC93000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evCatchTimer [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -0000000000000039CA53100000135AC93000000000000000 -00000000000028C82000000000000028C820000000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000005C710026BCB87678BCB620017C500000000000 -0000000005C50015BB52000000025BB51005C50000000000 -000000004C50029B510000000000015B92005C4000000000 -00000002B7002B9200000000000000029B2007B200000000 -00000008A102B810000000000000000018B201A800000000 -0000003C3019910000000000000000000199103C30000000 -00000098005B20000000013331000000002B500890000000 -000003C202B5000000039BBBBB9300000005B202C3000000 -000007A006B10000006C8312138C60000001B600A7000000 -00000B501B50000006B400151004B60000005B105B000000 -00002C303C2000003C40001510004C3000002C303C200000 -00003B105B000000980000151000089000000B501B300000 -00005B0078000001B3000015100003B1000008700B500000 -00006A0097000003B1000015111001B3000007900A600000 -0000690096000003B0000015555300B30000069009600000 -00006A0097000003B1000001111001B3000007900A600000 -00005B0078000001B3000000000003B1000008700B500000 -00003B105B000000980000000000089000000B501B300000 -00002C303C2000003C40000000004C3000002C303C200000 -00000B501B50000006B400000004B60000005B105B000000 -000007A006B10000006C8310138C60000001B600A7000000 -000003C202B5000000039BBBBB9300000005B202C3000000 -00000098005B20000000013331000000002B500890000000 -0000003C3019910000000000000000000199103C30000000 -00000008A102B810000000000000000018B201A800000000 -00000002B7002B9200000000000000029B2007B200000000 -000000004C50029B510000000000015B92005C4000000000 -0000000005C50015BB52000000025BB51005C50000000000 -00000000005C710026BCB87678BCB620017C500000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000000028C82000000000000028C820000000000000 -0000000000000039CA53100000135AC93000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evCatchSignal [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -0000000000000039CA53100000135AC93000000000000000 -00000000000028C82000000000000028C820000000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000005C710026BCB87678BCB620017C500000000000 -0000000005C50015BB52000000025BB51005C50000000000 -000000004C50029B510000000000015B92005C4000000000 -00000002B7002B9200000000000000029B2007B200000000 -00000008A102B810000000000000000018B201A800000000 -0000003C3019910000000000000000000199103C30000000 -00000098005B20000000000300000000002B500890000000 -000003C202B5000000000037300000000005B202C3000000 -000007A006B1000000000164610000000001B600A7000000 -00000B501B500000000003515300000000005B105B000000 -00002C303C200000000016202610000000002C303C200000 -00003B105B000000000045000540000000000B501B300000 -00005B00780000000001620002610000000008700B500000 -00006A00970000000004500000540000000007900A600000 -000069009600000000262000002620000000069009600000 -00006A00970000000054000000045000000007900A600000 -00005B00780000000261000000016200000008700B500000 -00003B105B000000054000000000450000000B501B300000 -00002C303C200000261000000000162000002C303C200000 -00000B501B500000541111111111145000005B105B000000 -000007A006B1000266666666666666620001B600A7000000 -000003C202B5000011111111111111100005B202C3000000 -00000098005B20000000000000000000002B500890000000 -0000003C3019910000000000000000000199103C30000000 -00000008A102B810000000000000000018B201A800000000 -00000002B7002B9200000000000000029B2007B200000000 -000000004C50029B510000000000015B92005C4000000000 -0000000005C50015BB52000000025BB51005C50000000000 -00000000005C710026BCB87678BCB620017C500000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000000028C82000000000000028C820000000000000 -0000000000000039CA53100000135AC93000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evCatchError [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -0000000000000039CA53100000135AC93000000000000000 -00000000000028C82000000000000028C820000000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000005C710026BCB87678BCB620017C500000000000 -0000000005C50015BB52000000025BB51005C50000000000 -000000004C50029B510000000000015B92005C4000000000 -00000002B7002B9200000000000000029B2007B200000000 -00000008A102B810000000000000000018B201A800000000 -0000003C3019910000000000100000000199103C30000000 -00000098005B20000000000151000000002B500890000000 -000003C202B5000000000004500000000005B202C3000000 -000007A006B1000000000016200000000001B600A7000000 -00000B501B500000000000450000000000005B105B000000 -00002C303C200000000001630000000000002C303C200000 -00003B105B000000000003610000000000000B501B300000 -00005B00780000000000164111100000000008700B500000 -00006A00970000000000366666610000000007900A600000 -000069009600000000006211144000000000069009600000 -00006A00970000000002644426200000000007900A600000 -00005B00780000000002444555000000000008700B500000 -00003B105B000000000000066200000000000B501B300000 -00002C303C200000000000166000000000002C303C200000 -00000B501B500000000000273000000000005B105B000000 -000007A006B1000000000046100000000001B600A7000000 -000003C202B5000000000064000000000005B202C3000000 -00000098005B20000000015100000000002B500890000000 -0000003C3019910000000010000000000199103C30000000 -00000008A102B810000000000000000018B201A800000000 -00000002B7002B9200000000000000029B2007B200000000 -000000004C50029B510000000000015B92005C4000000000 -0000000005C50015BB52000000025BB51005C50000000000 -00000000005C710026BCB87678BCB620017C500000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000000028C82000000000000028C820000000000000 -0000000000000039CA53100000135AC93000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evCatchEsc [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -0000000000000039CA53100000135AC93000000000000000 -00000000000028C82000000000000028C820000000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000005C710026BCB87678BCB620017C500000000000 -0000000005C50015BB52000000025BB51005C50000000000 -000000004C50029B510000000000015B92005C4000000000 -00000002B7002B9200000000000000029B2007B200000000 -00000008A102B810000000000000000018B201A800000000 -0000003C3019910000000000000000000199103C30000000 -00000098005B20000000000000000000002B500890000000 -000003C202B5000000000000000000000005B202C3000000 -000007A006B1000000000000000000000001B600A7000000 -00000B501B500000000000000000000000005B105B000000 -00002C303C200000000000000000000000002C303C200000 -00003B105B000000000000010000000000000B501B300000 -00005B00780000000000002620000000000008700B500000 -00006A00970000000000026662000000000007900A600000 -000069009600000000002636362000000000069009600000 -00006A00970000000002621612620000000007900A600000 -00005B00780000000026201610262000000008700B500000 -00003B105B000000026311262113620000000B501B300000 -00002C303C200000166666666666661000002C303C200000 -00000B501B500000011111262111110000005B105B000000 -000007A006B1000000000016100000000001B600A7000000 -000003C202B5000000000015100000000005B202C3000000 -00000098005B20000000000100000000002B500890000000 -0000003C3019910000000000000000000199103C30000000 -00000008A102B810000000000000000018B201A800000000 -00000002B7002B9200000000000000029B2007B200000000 -000000004C50029B510000000000015B92005C4000000000 -0000000005C50015BB52000000025BB51005C50000000000 -00000000005C710026BCB87678BCB620017C500000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000000028C82000000000000028C820000000000000 -0000000000000039CA53100000135AC93000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evCatchCond [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -0000000000000039CA53100000135AC93000000000000000 -00000000000028C82000000000000028C820000000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000005C710026BCB87678BCB620017C500000000000 -0000000005C50015BB52000000025BB51005C50000000000 -000000004C50029B510000000000015B92005C4000000000 -00000002B7002B9200000000000000029B2007B200000000 -00000008A102B810000000000000000018B201A800000000 -0000003C3019910004888888888884000199103C30000000 -00000098005B20004BFFFFFFFFFFFB40002B500890000000 -000003C202B500008FB888888888BF800005B202C3000000 -000007A006B100008F80000000008F800001B600A7000000 -00000B501B5000008F80000000008F8000005B105B000000 -00002C303C2000008F80000000008F8000002C303C200000 -00003B105B0000008F82555555528F8000000B501B300000 -00005B00780000008F80000000008F80000008700B500000 -00006A00970000008F80000000008F80000007900A600000 -00006900960000008F82555555528F800000069009600000 -00006A00970000008F80000000008F80000007900A600000 -00005B00780000008F80000000008F80000008700B500000 -00003B105B0000008F82555555528F8000000B501B300000 -00002C303C2000008F80000000008F8000002C303C200000 -00000B501B5000008F80000000008F8000005B105B000000 -000007A006B100008F80000000008F800001B600A7000000 -000003C202B500008FB888888888BF800005B202C3000000 -00000098005B20004BFFFFFFFFFFFB40002B500890000000 -0000003C3019910004888888888884000199103C30000000 -00000008A102B810000000000000000018B201A800000000 -00000002B7002B9200000000000000029B2007B200000000 -000000004C50029B510000000000015B92005C4000000000 -0000000005C50015BB52000000025BB51005C50000000000 -00000000005C710026BCB87678BCB620017C500000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000000028C82000000000000028C820000000000000 -0000000000000039CA53100000135AC93000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evCatchLink [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -0000000000000039CA53100000135AC93000000000000000 -00000000000028C82000000000000028C820000000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000005C710026BCB87678BCB620017C500000000000 -0000000005C50015BB52000000025BB51005C50000000000 -000000004C50029B510000000000015B92005C4000000000 -00000002B7002B9200000000000000029B2007B200000000 -00000008A102B810000000000000000018B201A800000000 -0000003C3019910000000000000000000199103C30000000 -00000098005B20000000000000000000002B500890000000 -000003C202B5000000000000000000000005B202C3000000 -000007A006B1000000000000000000000001B600A7000000 -00000B501B500000000000001000000000005B105B000000 -00002C303C200000000000016200000000002C303C200000 -00003B105B000000000000002620000000000B501B300000 -00005B00780000000000000002620000000008700B500000 -00006A00970000011111111111362000000007900A600000 -000069009600001566666666666661000000069009600000 -00006A00970000011111111111362000000007900A600000 -00005B00780000000000000002620000000008700B500000 -00003B105B000000000000002620000000000B501B300000 -00002C303C200000000000016200000000002C303C200000 -00000B501B500000000000001000000000005B105B000000 -000007A006B1000000000000000000000001B600A7000000 -000003C202B5000000000000000000000005B202C3000000 -00000098005B20000000000000000000002B500890000000 -0000003C3019910000000000000000000199103C30000000 -00000008A102B810000000000000000018B201A800000000 -00000002B7002B9200000000000000029B2007B200000000 -000000004C50029B510000000000015B92005C4000000000 -0000000005C50015BB52000000025BB51005C50000000000 -00000000005C710026BCB87678BCB620017C500000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000000028C82000000000000028C820000000000000 -0000000000000039CA53100000135AC93000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evCatchComp [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -0000000000000039CA53100000135AC93000000000000000 -00000000000028C82000000000000028C820000000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000005C710026BCB87678BCB620017C500000000000 -0000000005C50015BB52000000025BB51005C50000000000 -000000004C50029B510000000000015B92005C4000000000 -00000002B7002B9200000000000000029B2007B200000000 -00000008A102B810000000000000000018B201A800000000 -0000003C3019910000000000000000000199103C30000000 -00000098005B20000000000000000000002B500890000000 -000003C202B5000100000001000000000005B202C3000000 -000007A006B1000541000026200000000001B600A7000000 -00000B501B500005551000266300000000005B105B000000 -00002C303C200005346200252640000000002C303C200000 -00003B105B000005203630250155100000000B501B300000 -00005B00780000052002543500145200000008700B500000 -00006A00970000052000155500003630000007900A600000 -000069009600000520000155000003610000069009600000 -00006A00970000052000155500003630000007900A600000 -00005B00780000052002543500145200000008700B500000 -00003B105B000005203630250155100000000B501B300000 -00002C303C200005346200252640000000002C303C200000 -00000B501B500005551000266300000000005B105B000000 -000007A006B1000541000026200000000001B600A7000000 -000003C202B5000100000001000000000005B202C3000000 -00000098005B20000000000000000000002B500890000000 -0000003C3019910000000000000000000199103C30000000 -00000008A102B810000000000000000018B201A800000000 -00000002B7002B9200000000000000029B2007B200000000 -000000004C50029B510000000000015B92005C4000000000 -0000000005C50015BB52000000025BB51005C50000000000 -00000000005C710026BCB87678BCB620017C500000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000000028C82000000000000028C820000000000000 -0000000000000039CA53100000135AC93000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evCatchCancel [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -0000000000000039CA53100000135AC93000000000000000 -00000000000028C82000000000000028C820000000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000005C710026BCB87678BCB620017C500000000000 -0000000005C50015BB52000000025BB51005C50000000000 -000000004C50029B510000000000015B92005C4000000000 -00000002B7002B9200000000000000029B2007B200000000 -00000008A102B810000000000000000018B201A800000000 -0000003C3019910000000000000000000199103C30000000 -00000098005B20000000000000000000002B500890000000 -000003C202B5000001000000000001000005B202C3000000 -000007A006B1000016300000000036100001B600A7000000 -00000B501B500000036300000003630000005B105B000000 -00002C303C200000003630000036300000002C303C200000 -00003B105B000000000363000363000000000B501B300000 -00005B00780000000000363036300000000008700B500000 -00006A00970000000000036463000000000007900A600000 -000069009600000000000048400000000000069009600000 -00006A00970000000000036463000000000007900A600000 -00005B00780000000000363036300000000008700B500000 -00003B105B000000000363000363000000000B501B300000 -00002C303C200000003630000036300000002C303C200000 -00000B501B500000036300000003630000005B105B000000 -000007A006B1000016300000000036100001B600A7000000 -000003C202B5000001000000000001000005B202C3000000 -00000098005B20000000000000000000002B500890000000 -0000003C3019910000000000000000000199103C30000000 -00000008A102B810000000000000000018B201A800000000 -00000002B7002B9200000000000000029B2007B200000000 -000000004C50029B510000000000015B92005C4000000000 -0000000005C50015BB52000000025BB51005C50000000000 -00000000005C710026BCB87678BCB620017C500000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000000028C82000000000000028C820000000000000 -0000000000000039CA53100000135AC93000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evCatchMulti [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -0000000000000039CA53100000135AC93000000000000000 -00000000000028C82000000000000028C820000000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000005C710026BCB87678BCB620017C500000000000 -0000000005C50015BB52000000025BB51005C50000000000 -000000004C50029B510000000000015B92005C4000000000 -00000002B7002B9200000000000000029B2007B200000000 -00000008A102B810000000000000000018B201A800000000 -0000003C3019910000000000000000000199103C30000000 -00000098005B20000000001310000000002B500890000000 -000003C202B5000000000156510000000005B202C3000000 -000007A006B1000000003631363000000001B600A7000000 -00000B501B500000001462000264100000005B105B000000 -00002C303C200000025510000015520000002C303C200000 -00003B105B000000363000000000363000000B501B300000 -00005B00780000016200000000000261000008700B500000 -00006A00970000005300000000000350000007900A600000 -000069009600000025000000000005200000069009600000 -00006A00970000001610000000001610000007900A600000 -00005B00780000000530000000003500000008700B500000 -00003B105B000000035000000000530000000B501B300000 -00002C303C200000016100000001610000002C303C200000 -00000B501B500000005311111113500000005B105B000000 -000007A006B1000000366666666630000001B600A7000000 -000003C202B5000000011111111100000005B202C3000000 -00000098005B20000000000000000000002B500890000000 -0000003C3019910000000000000000000199103C30000000 -00000008A102B810000000000000000018B201A800000000 -00000002B7002B9200000000000000029B2007B200000000 -000000004C50029B510000000000015B92005C4000000000 -0000000005C50015BB52000000025BB51005C50000000000 -00000000005C710026BCB87678BCB620017C500000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000000028C82000000000000028C820000000000000 -0000000000000039CA53100000135AC93000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evCatchPar [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -0000000000000039CA53100000135AC93000000000000000 -00000000000028C82000000000000028C820000000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000005C710026BCB87678BCB620017C500000000000 -0000000005C50015BB52000000025BB51005C50000000000 -000000004C50029B510000000000015B92005C4000000000 -00000002B7002B9200000000000000029B2007B200000000 -00000008A102B810000000000000000018B201A800000000 -0000003C3019910000000000000000000199103C30000000 -00000098005B20000000135653100000002B500890000000 -000003C202B500000015BBA9ABB510000005B202C3000000 -000007A006B10000019B5101015B91000001B600A7000000 -00000B501B50000019A200151002A91000005B105B000000 -00002C303C2000005B20001610002B5000002C303C200000 -00003B105B000001B5000016100005B100000B501B300000 -00005B0078000003B1000016100001B3000008700B500000 -00006A0097000005A0111126211110A5000007900A600000 -000069009600000691566666666651960000069009600000 -00006A0097000005A0111126211110A5000007900A600000 -00005B0078000003B1000016100001B3000008700B500000 -00003B105B000001B5000016100005B100000B501B300000 -00002C303C2000005B20001610002B5000002C303C200000 -00000B501B50000019A200151002A91000005B105B000000 -000007A006B10000019B5101015B91000001B600A7000000 -000003C202B500000015BBA9ABB510000005B202C3000000 -00000098005B20000000135653100000002B500890000000 -0000003C3019910000000000000000000199103C30000000 -00000008A102B810000000000000000018B201A800000000 -00000002B7002B9200000000000000029B2007B200000000 -000000004C50029B510000000000015B92005C4000000000 -0000000005C50015BB52000000025BB51005C50000000000 -00000000005C710026BCB87678BCB620017C500000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000000028C82000000000000028C820000000000000 -0000000000000039CA53100000135AC93000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evThrowMsg [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -0000000000000039CA53100000135AC93000000000000000 -00000000000028C82000000000000028C820000000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000005C710026BCB87678BCB620017C500000000000 -0000000005C50015BB52000000025BB51005C50000000000 -000000004C50029B510000000000015B92005C4000000000 -00000002B7002B9200000000000000029B2007B200000000 -00000008A102B810000000000000000018B201A800000000 -0000003C3019910000000000000000000199103C30000000 -00000098005B20000000000000000000002B500890000000 -000003C202B5000000000000000000000005B202C3000000 -000007A006B1000888888888888888880001B600A7000000 -00000B501B50008FFFFFFFFFFFFFFFFF80005B105B000000 -00002C303C20008FFFFFFFFFFFFFFFFF80002C303C200000 -00003B105B00008FFFFFFFFFFFFFFFFF80000B501B300000 -00005B007800008FFFFFFFFFFFFFFFFF800008700B500000 -00006A009700008FFFFFFFFFFFFFFFFF800007900A600000 -000069009600008FFFFFFFFFFFFFFFFF8000069009600000 -00006A009700008FFFFFFFFFFFFFFFFF800007900A600000 -00005B007800008FFFFFFFFFFFFFFFFF800008700B500000 -00003B105B00008FFFFFFFFFFFFFFFFF80000B501B300000 -00002C303C20008FFFFFFFFFFFFFFFFF80002C303C200000 -00000B501B50008FFFFFFFFFFFFFFFFF80005B105B000000 -000007A006B1000888888888888888880001B600A7000000 -000003C202B5000000000000000000000005B202C3000000 -00000098005B20000000000000000000002B500890000000 -0000003C3019910000000000000000000199103C30000000 -00000008A102B810000000000000000018B201A800000000 -00000002B7002B9200000000000000029B2007B200000000 -000000004C50029B510000000000015B92005C4000000000 -0000000005C50015BB52000000025BB51005C50000000000 -00000000005C710026BCB87678BCB620017C500000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000000028C82000000000000028C820000000000000 -0000000000000039CA53100000135AC93000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evThrowSignal [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -0000000000000039CA53100000135AC93000000000000000 -00000000000028C82000000000000028C820000000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000005C710026BCB87678BCB620017C500000000000 -0000000005C50015BB52000000025BB51005C50000000000 -000000004C50029B510000000000015B92005C4000000000 -00000002B7002B9200000000000000029B2007B200000000 -00000008A102B810000000000000000018B201A800000000 -0000003C3019910000000000000000000199103C30000000 -00000098005B20000000000300000000002B500890000000 -000003C202B500000000003F300000000005B202C3000000 -000007A006B10000000001AFA10000000001B600A7000000 -00000B501B500000000006FFF600000000005B105B000000 -00002C303C20000000001CFFFC10000000002C303C200000 -00003B105B00000000006FFFFF60000000000B501B300000 -00005B00780000000001CFFFFFC10000000008700B500000 -00006A00970000000006FFFFFFF60000000007900A600000 -0000690096000000002DFFFFFFFD20000000069009600000 -00006A0097000000007FFFFFFFFF7000000007900A600000 -00005B007800000002DFFFFFFFFFD200000008700B500000 -00003B105B00000009FFFFFFFFFFF90000000B501B300000 -00002C303C2000002FFFFFFFFFFFFF2000002C303C200000 -00000B501B5000009FFFFFFFFFFFFF9000005B105B000000 -000007A006B10002FFFFFFFFFFFFFFF20001B600A7000000 -000003C202B5000011111111111111100005B202C3000000 -00000098005B20000000000000000000002B500890000000 -0000003C3019910000000000000000000199103C30000000 -00000008A102B810000000000000000018B201A800000000 -00000002B7002B9200000000000000029B2007B200000000 -000000004C50029B510000000000015B92005C4000000000 -0000000005C50015BB52000000025BB51005C50000000000 -00000000005C710026BCB87678BCB620017C500000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000000028C82000000000000028C820000000000000 -0000000000000039CA53100000135AC93000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evThrowEsc [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -0000000000000039CA53100000135AC93000000000000000 -00000000000028C82000000000000028C820000000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000005C710026BCB87678BCB620017C500000000000 -0000000005C50015BB52000000025BB51005C50000000000 -000000004C50029B510000000000015B92005C4000000000 -00000002B7002B9200000000000000029B2007B200000000 -00000008A102B810000000000000000018B201A800000000 -0000003C3019910000000000000000000199103C30000000 -00000098005B20000000000000000000002B500890000000 -000003C202B5000000000000000000000005B202C3000000 -000007A006B1000000000000000000000001B600A7000000 -00000B501B500000000000000000000000005B105B000000 -00002C303C200000000000000000000000002C303C200000 -00003B105B000000000000010000000000000B501B300000 -00005B00780000000000002620000000000008700B500000 -00006A00970000000000026662000000000007900A600000 -000069009600000000002636362000000000069009600000 -00006A00970000000002621612620000000007900A600000 -00005B00780000000026201610262000000008700B500000 -00003B105B000000026311262113620000000B501B300000 -00002C303C200000166666666666661000002C303C200000 -00000B501B500000011111262111110000005B105B000000 -000007A006B1000000000016100000000001B600A7000000 -000003C202B5000000000015100000000005B202C3000000 -00000098005B20000000000100000000002B500890000000 -0000003C3019910000000000000000000199103C30000000 -00000008A102B810000000000000000018B201A800000000 -00000002B7002B9200000000000000029B2007B200000000 -000000004C50029B510000000000015B92005C4000000000 -0000000005C50015BB52000000025BB51005C50000000000 -00000000005C710026BCB87678BCB620017C500000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000000028C82000000000000028C820000000000000 -0000000000000039CA53100000135AC93000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evThrowLink [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -0000000000000039CA53100000135AC93000000000000000 -00000000000028C82000000000000028C820000000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000005C710026BCB87678BCB620017C500000000000 -0000000005C50015BB52000000025BB51005C50000000000 -000000004C50029B510000000000015B92005C4000000000 -00000002B7002B9200000000000000029B2007B200000000 -00000008A102B810000000000000000018B201A800000000 -0000003C3019910000000000000000000199103C30000000 -00000098005B20000000000000000000002B500890000000 -000003C202B5000000000000000000000005B202C3000000 -000007A006B1000000000000000000000001B600A7000000 -00000B501B500000000000001000000000005B105B000000 -00002C303C200000000000016200000000002C303C200000 -00003B105B000000000000002620000000000B501B300000 -00005B00780000000000000002620000000008700B500000 -00006A00970000011111111111362000000007900A600000 -000069009600001566666666666661000000069009600000 -00006A00970000011111111111362000000007900A600000 -00005B00780000000000000002620000000008700B500000 -00003B105B000000000000002620000000000B501B300000 -00002C303C200000000000016200000000002C303C200000 -00000B501B500000000000001000000000005B105B000000 -000007A006B1000000000000000000000001B600A7000000 -000003C202B5000000000000000000000005B202C3000000 -00000098005B20000000000000000000002B500890000000 -0000003C3019910000000000000000000199103C30000000 -00000008A102B810000000000000000018B201A800000000 -00000002B7002B9200000000000000029B2007B200000000 -000000004C50029B510000000000015B92005C4000000000 -0000000005C50015BB52000000025BB51005C50000000000 -00000000005C710026BCB87678BCB620017C500000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000000028C82000000000000028C820000000000000 -0000000000000039CA53100000135AC93000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evThrowComp [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -0000000000000039CA53100000135AC93000000000000000 -00000000000028C82000000000000028C820000000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000005C710026BCB87678BCB620017C500000000000 -0000000005C50015BB52000000025BB51005C50000000000 -000000004C50029B510000000000015B92005C4000000000 -00000002B7002B9200000000000000029B2007B200000000 -00000008A102B810000000000000000018B201A800000000 -0000003C3019910000000000000000000199103C30000000 -00000098005B20000000000000000000002B500890000000 -000003C202B5000100000001000000000005B202C3000000 -000007A006B1000541000026200000000001B600A7000000 -00000B501B500005551000266300000000005B105B000000 -00002C303C200005346200252640000000002C303C200000 -00003B105B000005203630250155100000000B501B300000 -00005B00780000052002543500145200000008700B500000 -00006A00970000052000155500003630000007900A600000 -000069009600000520000155000003610000069009600000 -00006A00970000052000155500003630000007900A600000 -00005B00780000052002543500145200000008700B500000 -00003B105B000005203630250155100000000B501B300000 -00002C303C200005346200252640000000002C303C200000 -00000B501B500005551000266300000000005B105B000000 -000007A006B1000541000026200000000001B600A7000000 -000003C202B5000100000001000000000005B202C3000000 -00000098005B20000000000000000000002B500890000000 -0000003C3019910000000000000000000199103C30000000 -00000008A102B810000000000000000018B201A800000000 -00000002B7002B9200000000000000029B2007B200000000 -000000004C50029B510000000000015B92005C4000000000 -0000000005C50015BB52000000025BB51005C50000000000 -00000000005C710026BCB87678BCB620017C500000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000000028C82000000000000028C820000000000000 -0000000000000039CA53100000135AC93000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evThrowMulti [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -0000000000000039CA53100000135AC93000000000000000 -00000000000028C82000000000000028C820000000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000005C710026BCB87678BCB620017C500000000000 -0000000005C50015BB52000000025BB51005C50000000000 -000000004C50029B510000000000015B92005C4000000000 -00000002B7002B9200000000000000029B2007B200000000 -00000008A102B810000000000000000018B201A800000000 -0000003C3019910000000000000000000199103C30000000 -00000098005B20000000001310000000002B500890000000 -000003C202B5000000000156510000000005B202C3000000 -000007A006B1000000003631363000000001B600A7000000 -00000B501B500000001462000264100000005B105B000000 -00002C303C200000025510000015520000002C303C200000 -00003B105B000000363000000000363000000B501B300000 -00005B00780000016200000000000261000008700B500000 -00006A00970000005300000000000350000007900A600000 -000069009600000025000000000005200000069009600000 -00006A00970000001610000000001610000007900A600000 -00005B00780000000530000000003500000008700B500000 -00003B105B000000035000000000530000000B501B300000 -00002C303C200000016100000001610000002C303C200000 -00000B501B500000005311111113500000005B105B000000 -000007A006B1000000366666666630000001B600A7000000 -000003C202B5000000011111111100000005B202C3000000 -00000098005B20000000000000000000002B500890000000 -0000003C3019910000000000000000000199103C30000000 -00000008A102B810000000000000000018B201A800000000 -00000002B7002B9200000000000000029B2007B200000000 -000000004C50029B510000000000015B92005C4000000000 -0000000005C50015BB52000000025BB51005C50000000000 -00000000005C710026BCB87678BCB620017C500000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000000028C82000000000000028C820000000000000 -0000000000000039CA53100000135AC93000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evEnd [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000001111111000000000000000000000 -000000000000000001345677765431000000000000000000 -0000000000000013579ABCCDCCBA97531000000000000000 -0000000000000358ACEEDCCBCCDEECA85300000000000000 -00000000000148BDECA876666678ACEDB841000000000000 -0000000000269CEB974321111123479BEC96200000000000 -00000000026ADD9641000000000001469DDA620000000000 -0000000026AEC841000000000000000148CEA62000000000 -000000016AEB73000000000000000000037BEA6100000000 -000000049DC7300000000000000000000037CD9400000000 -00000038CD830000000000000000000000038DC830000000 -0000015BE94000000000000000000000000049EB51000000 -0000038DB61000000000000000000000000016BD83000000 -000005AE9400000000000000000000000000049EA5000000 -000017CC7100000000000000000000000000017CC7100000 -000039EA4000000000000000000000000000004AE9300000 -00004AE830000000000000000000000000000038EA400000 -00015BD720000000000000000000000000000027DB510000 -00016CC610000000000000000000000000000016CC610000 -00017CC610000000000000000000000000000016CC710000 -00017DB610000000000000000000000000000016BD710000 -00017CC610000000000000000000000000000016CC710000 -00016CC610000000000000000000000000000016CC610000 -00015BD720000000000000000000000000000027DB510000 -00004AE830000000000000000000000000000038EA400000 -000039EA4000000000000000000000000000004AE9300000 -000017CC7100000000000000000000000000017CC7100000 -000005AE9400000000000000000000000000049EA5000000 -0000038DB61000000000000000000000000016BD83000000 -0000015BE94000000000000000000000000049EB51000000 -00000038CD830000000000000000000000038DC830000000 -000000049DC7300000000000000000000037CD9400000000 -000000016AEB73000000000000000000037BEA6100000000 -0000000026AEC841000000000000000148CEA62000000000 -00000000026ADD9641000000000001469DDA620000000000 -0000000000269CEB974321111123479BEC96200000000000 -00000000000148BDECA876666678ACEDB841000000000000 -0000000000000358ACEEDCCBCCDEECA85300000000000000 -0000000000000013579ABCCDCCBA97531000000000000000 -000000000000000001345677765431000000000000000000 -000000000000000000001111111000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evEndMsg [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000001111111000000000000000000000 -000000000000000001345677765431000000000000000000 -0000000000000013579ABCCDCCBA97531000000000000000 -0000000000000358ACEEDCCBCCDEECA85300000000000000 -00000000000148BDECA876666678ACEDB841000000000000 -0000000000269CEB974321111123479BEC96200000000000 -00000000026ADD9641000000000001469DDA620000000000 -0000000026AEC841000000000000000148CEA62000000000 -000000016AEB73000000000000000000037BEA6100000000 -000000049DC7300000000000000000000037CD9400000000 -00000038CD830000000000000000000000038DC830000000 -0000015BE94000000000000000000000000049EB51000000 -0000038DB61000000000000000000000000016BD83000000 -000005AE9400000000000000000000000000049EA5000000 -000017CC7100000888888888888888880000017CC7100000 -000039EA4000008FFFFFFFFFFFFFFFFF8000004AE9300000 -00004AE83000008FFFFFFFFFFFFFFFFF80000038EA400000 -00015BD72000008FFFFFFFFFFFFFFFFF80000027DB510000 -00016CC61000008FFFFFFFFFFFFFFFFF80000016CC610000 -00017CC61000008FFFFFFFFFFFFFFFFF80000016CC710000 -00017DB61000008FFFFFFFFFFFFFFFFF80000016BD710000 -00017CC61000008FFFFFFFFFFFFFFFFF80000016CC710000 -00016CC61000008FFFFFFFFFFFFFFFFF80000016CC610000 -00015BD72000008FFFFFFFFFFFFFFFFF80000027DB510000 -00004AE83000008FFFFFFFFFFFFFFFFF80000038EA400000 -000039EA4000008FFFFFFFFFFFFFFFFF8000004AE9300000 -000017CC7100000888888888888888880000017CC7100000 -000005AE9400000000000000000000000000049EA5000000 -0000038DB61000000000000000000000000016BD83000000 -0000015BE94000000000000000000000000049EB51000000 -00000038CD830000000000000000000000038DC830000000 -000000049DC7300000000000000000000037CD9400000000 -000000016AEB73000000000000000000037BEA6100000000 -0000000026AEC841000000000000000148CEA62000000000 -00000000026ADD9641000000000001469DDA620000000000 -0000000000269CEB974321111123479BEC96200000000000 -00000000000148BDECA876666678ACEDB841000000000000 -0000000000000358ACEEDCCBCCDEECA85300000000000000 -0000000000000013579ABCCDCCBA97531000000000000000 -000000000000000001345677765431000000000000000000 -000000000000000000001111111000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evEndError [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000001111111000000000000000000000 -000000000000000001345677765431000000000000000000 -0000000000000013579ABCCDCCBA97531000000000000000 -0000000000000358ACEEDCCBCCDEECA85300000000000000 -00000000000148BDECA876666678ACEDB841000000000000 -0000000000269CEB974321111123479BEC96200000000000 -00000000026ADD9641000000000001469DDA620000000000 -0000000026AEC841000000000000000148CEA62000000000 -000000016AEB73000000000000000000037BEA6100000000 -000000049DC7300000000000000000000037CD9400000000 -00000038CD830000000000000000000000038DC830000000 -0000015BE94000000000000010000000000049EB51000000 -0000038DB61000000000000151000000000016BD83000000 -000005AE9400000000000004500000000000049EA5000000 -000017CC7100000000000016200000000000017CC7100000 -000039EA4000000000000045000000000000004AE9300000 -00004AE830000000000001630000000000000038EA400000 -00015BD720000000000003610000000000000027DB510000 -00016CC610000000000016411110000000000016CC610000 -00017CC610000000000036666661000000000016CC710000 -00017DB610000000000062111440000000000016BD710000 -00017CC610000000000264442620000000000016CC710000 -00016CC610000000000244455500000000000016CC610000 -00015BD720000000000000066200000000000027DB510000 -00004AE830000000000000166000000000000038EA400000 -000039EA4000000000000027300000000000004AE9300000 -000017CC7100000000000046100000000000017CC7100000 -000005AE9400000000000064000000000000049EA5000000 -0000038DB61000000000015100000000000016BD83000000 -0000015BE94000000000001000000000000049EB51000000 -00000038CD830000000000000000000000038DC830000000 -000000049DC7300000000000000000000037CD9400000000 -000000016AEB73000000000000000000037BEA6100000000 -0000000026AEC841000000000000000148CEA62000000000 -00000000026ADD9641000000000001469DDA620000000000 -0000000000269CEB974321111123479BEC96200000000000 -00000000000148BDECA876666678ACEDB841000000000000 -0000000000000358ACEEDCCBCCDEECA85300000000000000 -0000000000000013579ABCCDCCBA97531000000000000000 -000000000000000001345677765431000000000000000000 -000000000000000000001111111000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evEndEsc [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000001111111000000000000000000000 -000000000000000001345677765431000000000000000000 -0000000000000013579ABCCDCCBA97531000000000000000 -0000000000000358ACEEDCCBCCDEECA85300000000000000 -00000000000148BDECA876666678ACEDB841000000000000 -0000000000269CEB974321111123479BEC96200000000000 -00000000026ADD9641000000000001469DDA620000000000 -0000000026AEC841000000000000000148CEA62000000000 -000000016AEB73000000000000000000037BEA6100000000 -000000049DC7300000000000000000000037CD9400000000 -00000038CD830000000000000000000000038DC830000000 -0000015BE94000000000000000000000000049EB51000000 -0000038DB61000000000000000000000000016BD83000000 -000005AE9400000000000000000000000000049EA5000000 -000017CC7100000000000000000000000000017CC7100000 -000039EA4000000000000000000000000000004AE9300000 -00004AE830000000000000000000000000000038EA400000 -00015BD720000000000000010000000000000027DB510000 -00016CC610000000000000262000000000000016CC610000 -00017CC610000000000002666200000000000016CC710000 -00017DB610000000000026363620000000000016BD710000 -00017CC610000000000262161262000000000016CC710000 -00016CC610000000002620161026200000000016CC610000 -00015BD720000000026311262113620000000027DB510000 -00004AE830000000166666666666661000000038EA400000 -000039EA4000000001111126211111000000004AE9300000 -000017CC7100000000000016100000000000017CC7100000 -000005AE9400000000000015100000000000049EA5000000 -0000038DB61000000000000100000000000016BD83000000 -0000015BE94000000000000000000000000049EB51000000 -00000038CD830000000000000000000000038DC830000000 -000000049DC7300000000000000000000037CD9400000000 -000000016AEB73000000000000000000037BEA6100000000 -0000000026AEC841000000000000000148CEA62000000000 -00000000026ADD9641000000000001469DDA620000000000 -0000000000269CEB974321111123479BEC96200000000000 -00000000000148BDECA876666678ACEDB841000000000000 -0000000000000358ACEEDCCBCCDEECA85300000000000000 -0000000000000013579ABCCDCCBA97531000000000000000 -000000000000000001345677765431000000000000000000 -000000000000000000001111111000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evEndCancel [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000001111111000000000000000000000 -000000000000000001345677765431000000000000000000 -0000000000000013579ABCCDCCBA97531000000000000000 -0000000000000358ACEEDCCBCCDEECA85300000000000000 -00000000000148BDECA876666678ACEDB841000000000000 -0000000000269CEB974321111123479BEC96200000000000 -00000000026ADD9641000000000001469DDA620000000000 -0000000026AEC841000000000000000148CEA62000000000 -000000016AEB73000000000000000000037BEA6100000000 -000000049DC7300000000000000000000037CD9400000000 -00000038CD830000000000000000000000038DC830000000 -0000015BE94000000000000000000000000049EB51000000 -0000038DB61000000000000000000000000016BD83000000 -000005AE9400000001000000000001000000049EA5000000 -000017CC7100000016300000000036100000017CC7100000 -000039EA4000000003630000000363000000004AE9300000 -00004AE830000000003630000036300000000038EA400000 -00015BD720000000000363000363000000000027DB510000 -00016CC610000000000036303630000000000016CC610000 -00017CC610000000000003646300000000000016CC710000 -00017DB610000000000000484000000000000016BD710000 -00017CC610000000000003646300000000000016CC710000 -00016CC610000000000036303630000000000016CC610000 -00015BD720000000000363000363000000000027DB510000 -00004AE830000000003630000036300000000038EA400000 -000039EA4000000003630000000363000000004AE9300000 -000017CC7100000016300000000036100000017CC7100000 -000005AE9400000001000000000001000000049EA5000000 -0000038DB61000000000000000000000000016BD83000000 -0000015BE94000000000000000000000000049EB51000000 -00000038CD830000000000000000000000038DC830000000 -000000049DC7300000000000000000000037CD9400000000 -000000016AEB73000000000000000000037BEA6100000000 -0000000026AEC841000000000000000148CEA62000000000 -00000000026ADD9641000000000001469DDA620000000000 -0000000000269CEB974321111123479BEC96200000000000 -00000000000148BDECA876666678ACEDB841000000000000 -0000000000000358ACEEDCCBCCDEECA85300000000000000 -0000000000000013579ABCCDCCBA97531000000000000000 -000000000000000001345677765431000000000000000000 -000000000000000000001111111000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evEndComp [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000001111111000000000000000000000 -000000000000000001345677765431000000000000000000 -0000000000000013579ABCCDCCBA97531000000000000000 -0000000000000358ACEEDCCBCCDEECA85300000000000000 -00000000000148BDECA876666678ACEDB841000000000000 -0000000000269CEB974321111123479BEC96200000000000 -00000000026ADD9641000000000001469DDA620000000000 -0000000026AEC841000000000000000148CEA62000000000 -000000016AEB73000000000000000000037BEA6100000000 -000000049DC7300000000000000000000037CD9400000000 -00000038CD830000000000000000000000038DC830000000 -0000015BE94000000000000000000000000049EB51000000 -0000038DB61000000000000000000000000016BD83000000 -000005AE9400000100000001000000000000049EA5000000 -000017CC7100000541000026200000000000017CC7100000 -000039EA4000000555100026630000000000004AE9300000 -00004AE830000005346200252640000000000038EA400000 -00015BD720000005203630250155100000000027DB510000 -00016CC610000005200254350014520000000016CC610000 -00017CC610000005200015550000363000000016CC710000 -00017DB610000005200001550000036100000016BD710000 -00017CC610000005200015550000363000000016CC710000 -00016CC610000005200254350014520000000016CC610000 -00015BD720000005203630250155100000000027DB510000 -00004AE830000005346200252640000000000038EA400000 -000039EA4000000555100026630000000000004AE9300000 -000017CC7100000541000026200000000000017CC7100000 -000005AE9400000100000001000000000000049EA5000000 -0000038DB61000000000000000000000000016BD83000000 -0000015BE94000000000000000000000000049EB51000000 -00000038CD830000000000000000000000038DC830000000 -000000049DC7300000000000000000000037CD9400000000 -000000016AEB73000000000000000000037BEA6100000000 -0000000026AEC841000000000000000148CEA62000000000 -00000000026ADD9641000000000001469DDA620000000000 -0000000000269CEB974321111123479BEC96200000000000 -00000000000148BDECA876666678ACEDB841000000000000 -0000000000000358ACEEDCCBCCDEECA85300000000000000 -0000000000000013579ABCCDCCBA97531000000000000000 -000000000000000001345677765431000000000000000000 -000000000000000000001111111000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evEndSignal [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000001111111000000000000000000000 -000000000000000001345677765431000000000000000000 -0000000000000013579ABCCDCCBA97531000000000000000 -0000000000000358ACEEDCCBCCDEECA85300000000000000 -00000000000148BDECA876666678ACEDB841000000000000 -0000000000269CEB974321111123479BEC96200000000000 -00000000026ADD9641000000000001469DDA620000000000 -0000000026AEC841000000000000000148CEA62000000000 -000000016AEB73000000000000000000037BEA6100000000 -000000049DC7300000000000000000000037CD9400000000 -00000038CD830000000000000000000000038DC830000000 -0000015BE94000000000000000000000000049EB51000000 -0000038DB61000000000000300000000000016BD83000000 -000005AE940000000000003F300000000000049EA5000000 -000017CC71000000000001AFA10000000000017CC7100000 -000039EA40000000000006FFF60000000000004AE9300000 -00004AE83000000000001CFFFC10000000000038EA400000 -00015BD72000000000006FFFFF60000000000027DB510000 -00016CC6100000000001CFFFFFC1000000000016CC610000 -00017CC6100000000006FFFFFFF6000000000016CC710000 -00017DB610000000002DFFFFFFFD200000000016BD710000 -00017CC610000000007FFFFFFFFF700000000016CC710000 -00016CC61000000002DFFFFFFFFFD20000000016CC610000 -00015BD72000000009FFFFFFFFFFF90000000027DB510000 -00004AE8300000002FFFFFFFFFFFFF2000000038EA400000 -000039EA400000009FFFFFFFFFFFFF900000004AE9300000 -000017CC71000002FFFFFFFFFFFFFFF20000017CC7100000 -000005AE9400000011111111111111100000049EA5000000 -0000038DB61000000000000000000000000016BD83000000 -0000015BE94000000000000000000000000049EB51000000 -00000038CD830000000000000000000000038DC830000000 -000000049DC7300000000000000000000037CD9400000000 -000000016AEB73000000000000000000037BEA6100000000 -0000000026AEC841000000000000000148CEA62000000000 -00000000026ADD9641000000000001469DDA620000000000 -0000000000269CEB974321111123479BEC96200000000000 -00000000000148BDECA876666678ACEDB841000000000000 -0000000000000358ACEEDCCBCCDEECA85300000000000000 -0000000000000013579ABCCDCCBA97531000000000000000 -000000000000000001345677765431000000000000000000 -000000000000000000001111111000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evEndMulti [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000001111111000000000000000000000 -000000000000000001345677765431000000000000000000 -0000000000000013579ABCCDCCBA97531000000000000000 -0000000000000358ACEEDCCBCCDEECA85300000000000000 -00000000000148BDECA876666678ACEDB841000000000000 -0000000000269CEB974321111123479BEC96200000000000 -00000000026ADD9641000000000001469DDA620000000000 -0000000026AEC841000000000000000148CEA62000000000 -000000016AEB73000000000000000000037BEA6100000000 -000000049DC7300000000000000000000037CD9400000000 -00000038CD830000000000000000000000038DC830000000 -0000015BE94000000000000000000000000049EB51000000 -0000038DB61000000000001310000000000016BD83000000 -000005AE9400000000000156510000000000049EA5000000 -000017CC7100000000003631363000000000017CC7100000 -000039EA4000000000146200026410000000004AE9300000 -00004AE830000000025510000015520000000038EA400000 -00015BD720000000363000000000363000000027DB510000 -00016CC610000001620000000000026100000016CC610000 -00017CC610000000530000000000035000000016CC710000 -00017DB610000000250000000000052000000016BD710000 -00017CC610000000161000000000161000000016CC710000 -00016CC610000000053000000000350000000016CC610000 -00015BD720000000035000000000530000000027DB510000 -00004AE830000000016100000001610000000038EA400000 -000039EA4000000000531111111350000000004AE9300000 -000017CC7100000000366666666630000000017CC7100000 -000005AE9400000000011111111100000000049EA5000000 -0000038DB61000000000000000000000000016BD83000000 -0000015BE94000000000000000000000000049EB51000000 -00000038CD830000000000000000000000038DC830000000 -000000049DC7300000000000000000000037CD9400000000 -000000016AEB73000000000000000000037BEA6100000000 -0000000026AEC841000000000000000148CEA62000000000 -00000000026ADD9641000000000001469DDA620000000000 -0000000000269CEB974321111123479BEC96200000000000 -00000000000148BDECA876666678ACEDB841000000000000 -0000000000000358ACEEDCCBCCDEECA85300000000000000 -0000000000000013579ABCCDCCBA97531000000000000000 -000000000000000001345677765431000000000000000000 -000000000000000000001111111000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evTerminate [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000001111111000000000000000000000 -000000000000000001345677765431000000000000000000 -0000000000000013579ABCCDCCBA97531000000000000000 -0000000000000358ACEEDCCBCCDEECA85300000000000000 -00000000000148BDECA876666678ACEDB841000000000000 -0000000000269CEB974321111123479BEC96200000000000 -00000000026ADD9641000000000001469DDA620000000000 -0000000026AEC841000000000000000148CEA62000000000 -000000016AEB73000000000000000000037BEA6100000000 -000000049DC7300000000000000000000037CD9400000000 -00000038CD830000000000000000000000038DC830000000 -0000015BE94000000000000000000000000049EB51000000 -0000038DB61000000000000100000000000016BD83000000 -000005AE940000000000379A973000000000049EA5000000 -000017CC710000000027CFFFFFC720000000017CC7100000 -000039EA40000000029EFFFFFFFE92000000004AE9300000 -00004AE83000000007EFFFFFFFFFE70000000038EA400000 -00015BD7200000003CFFFFFFFFFFFC3000000027DB510000 -00016CC6100000007FFFFFFFFFFFFF7000000016CC610000 -00017CC6100000009FFFFFFFFFFFFF9000000016CC710000 -00017DB610000001AFFFFFFFFFFFFFA100000016BD710000 -00017CC6100000009FFFFFFFFFFFFF9000000016CC710000 -00016CC6100000007FFFFFFFFFFFFF7000000016CC610000 -00015BD7200000003CFFFFFFFFFFFC3000000027DB510000 -00004AE83000000007EFFFFFFFFFE70000000038EA400000 -000039EA40000000029EFFFFFFFE92000000004AE9300000 -000017CC710000000027CFFFFFC720000000017CC7100000 -000005AE940000000000379A973000000000049EA5000000 -0000038DB61000000000000100000000000016BD83000000 -0000015BE94000000000000000000000000049EB51000000 -00000038CD830000000000000000000000038DC830000000 -000000049DC7300000000000000000000037CD9400000000 -000000016AEB73000000000000000000037BEA6100000000 -0000000026AEC841000000000000000148CEA62000000000 -00000000026ADD9641000000000001469DDA620000000000 -0000000000269CEB974321111123479BEC96200000000000 -00000000000148BDECA876666678ACEDB841000000000000 -0000000000000358ACEEDCCBCCDEECA85300000000000000 -0000000000000013579ABCCDCCBA97531000000000000000 -000000000000000001345677765431000000000000000000 -000000000000000000001111111000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evBoundMsg [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -0000000000000039CA53100000135AC93000000000000000 -00000000000028C82000000000000028C820000000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000005C710026BCB87678BCB620017C500000000000 -0000000005C50015BB52000000025BB51005C50000000000 -000000004C50029B510000000000015B92005C4000000000 -00000002B7002B9200000000000000029B2007B200000000 -00000008A102B810000000000000000018B201A800000000 -0000003C3019910000000000000000000199103C30000000 -00000098005B20000000000000000000002B500890000000 -000003C202B5000000000000000000000005B202C3000000 -000007A006B1000888888888888888880001B600A7000000 -00000B501B50008FFFFFFFFFFFFFFFFF80005B105B000000 -00002C303C20008FDA88888888888ADF80002C303C200000 -00003B105B00008F854000000000458F80000B501B300000 -00005B007800008F815510000015518F800008700B500000 -00006A009700008F800451000154008F800007900A600000 -000069009600008F800036202630008F8000069009600000 -00006A009700008F800002646200008F800007900A600000 -00005B007800008F800000252000008F800008700B500000 -00003B105B00008F800000000000008F80000B501B300000 -00002C303C20008FB8888888888888BF80002C303C200000 -00000B501B50008FFFFFFFFFFFFFFFFF80005B105B000000 -000007A006B1000888888888888888880001B600A7000000 -000003C202B5000000000000000000000005B202C3000000 -00000098005B20000000000000000000002B500890000000 -0000003C3019910000000000000000000199103C30000000 -00000008A102B810000000000000000018B201A800000000 -00000002B7002B9200000000000000029B2007B200000000 -000000004C50029B510000000000015B92005C4000000000 -0000000005C50015BB52000000025BB51005C50000000000 -00000000005C710026BCB87678BCB620017C500000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000000028C82000000000000028C820000000000000 -0000000000000039CA53100000135AC93000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evBoundTimer [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -0000000000000039CA53100000135AC93000000000000000 -00000000000028C82000000000000028C820000000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000005C710026BCB87678BCB620017C500000000000 -0000000005C50015BB52000000025BB51005C50000000000 -000000004C50029B510000000000015B92005C4000000000 -00000002B7002B9200000000000000029B2007B200000000 -00000008A102B810000000000000000018B201A800000000 -0000003C3019910000000000000000000199103C30000000 -00000098005B20000000013331000000002B500890000000 -000003C202B5000000039BBBBB9300000005B202C3000000 -000007A006B10000006C8312138C60000001B600A7000000 -00000B501B50000006B400151004B60000005B105B000000 -00002C303C2000003C40001510004C3000002C303C200000 -00003B105B000000980000151000089000000B501B300000 -00005B0078000001B3000015100003B1000008700B500000 -00006A0097000003B1000015111001B3000007900A600000 -0000690096000003B0000015555300B30000069009600000 -00006A0097000003B1000001111001B3000007900A600000 -00005B0078000001B3000000000003B1000008700B500000 -00003B105B000000980000000000089000000B501B300000 -00002C303C2000003C40000000004C3000002C303C200000 -00000B501B50000006B400000004B60000005B105B000000 -000007A006B10000006C8310138C60000001B600A7000000 -000003C202B5000000039BBBBB9300000005B202C3000000 -00000098005B20000000013331000000002B500890000000 -0000003C3019910000000000000000000199103C30000000 -00000008A102B810000000000000000018B201A800000000 -00000002B7002B9200000000000000029B2007B200000000 -000000004C50029B510000000000015B92005C4000000000 -0000000005C50015BB52000000025BB51005C50000000000 -00000000005C710026BCB87678BCB620017C500000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000000028C82000000000000028C820000000000000 -0000000000000039CA53100000135AC93000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evBoundError [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -0000000000000039CA53100000135AC93000000000000000 -00000000000028C82000000000000028C820000000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000005C710026BCB87678BCB620017C500000000000 -0000000005C50015BB52000000025BB51005C50000000000 -000000004C50029B510000000000015B92005C4000000000 -00000002B7002B9200000000000000029B2007B200000000 -00000008A102B810000000000000000018B201A800000000 -0000003C3019910000000000100000000199103C30000000 -00000098005B20000000000151000000002B500890000000 -000003C202B5000000000004500000000005B202C3000000 -000007A006B1000000000016200000000001B600A7000000 -00000B501B500000000000450000000000005B105B000000 -00002C303C200000000001630000000000002C303C200000 -00003B105B000000000003610000000000000B501B300000 -00005B00780000000000164111100000000008700B500000 -00006A00970000000000366666610000000007900A600000 -000069009600000000006211144000000000069009600000 -00006A00970000000002644426200000000007900A600000 -00005B00780000000002444555000000000008700B500000 -00003B105B000000000000066200000000000B501B300000 -00002C303C200000000000166000000000002C303C200000 -00000B501B500000000000273000000000005B105B000000 -000007A006B1000000000046100000000001B600A7000000 -000003C202B5000000000064000000000005B202C3000000 -00000098005B20000000015100000000002B500890000000 -0000003C3019910000000010000000000199103C30000000 -00000008A102B810000000000000000018B201A800000000 -00000002B7002B9200000000000000029B2007B200000000 -000000004C50029B510000000000015B92005C4000000000 -0000000005C50015BB52000000025BB51005C50000000000 -00000000005C710026BCB87678BCB620017C500000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000000028C82000000000000028C820000000000000 -0000000000000039CA53100000135AC93000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evBoundEsc [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -0000000000000039CA53100000135AC93000000000000000 -00000000000028C82000000000000028C820000000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000005C710026BCB87678BCB620017C500000000000 -0000000005C50015BB52000000025BB51005C50000000000 -000000004C50029B510000000000015B92005C4000000000 -00000002B7002B9200000000000000029B2007B200000000 -00000008A102B810000000000000000018B201A800000000 -0000003C3019910000000000000000000199103C30000000 -00000098005B20000000000000000000002B500890000000 -000003C202B5000000000000000000000005B202C3000000 -000007A006B1000000000000000000000001B600A7000000 -00000B501B500000000000000000000000005B105B000000 -00002C303C200000000000000000000000002C303C200000 -00003B105B000000000000010000000000000B501B300000 -00005B00780000000000002620000000000008700B500000 -00006A00970000000000026662000000000007900A600000 -000069009600000000002636362000000000069009600000 -00006A00970000000002621612620000000007900A600000 -00005B00780000000026201610262000000008700B500000 -00003B105B000000026311262113620000000B501B300000 -00002C303C200000166666666666661000002C303C200000 -00000B501B500000011111262111110000005B105B000000 -000007A006B1000000000016100000000001B600A7000000 -000003C202B5000000000015100000000005B202C3000000 -00000098005B20000000000100000000002B500890000000 -0000003C3019910000000000000000000199103C30000000 -00000008A102B810000000000000000018B201A800000000 -00000002B7002B9200000000000000029B2007B200000000 -000000004C50029B510000000000015B92005C4000000000 -0000000005C50015BB52000000025BB51005C50000000000 -00000000005C710026BCB87678BCB620017C500000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000000028C82000000000000028C820000000000000 -0000000000000039CA53100000135AC93000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evBoundSignal [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -0000000000000039CA53100000135AC93000000000000000 -00000000000028C82000000000000028C820000000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000005C710026BCB87678BCB620017C500000000000 -0000000005C50015BB52000000025BB51005C50000000000 -000000004C50029B510000000000015B92005C4000000000 -00000002B7002B9200000000000000029B2007B200000000 -00000008A102B810000000000000000018B201A800000000 -0000003C3019910000000000000000000199103C30000000 -00000098005B20000000000300000000002B500890000000 -000003C202B5000000000037300000000005B202C3000000 -000007A006B1000000000164610000000001B600A7000000 -00000B501B500000000003515300000000005B105B000000 -00002C303C200000000016202610000000002C303C200000 -00003B105B000000000045000540000000000B501B300000 -00005B00780000000001620002610000000008700B500000 -00006A00970000000004500000540000000007900A600000 -000069009600000000262000002620000000069009600000 -00006A00970000000054000000045000000007900A600000 -00005B00780000000261000000016200000008700B500000 -00003B105B000000054000000000450000000B501B300000 -00002C303C200000261000000000162000002C303C200000 -00000B501B500000541111111111145000005B105B000000 -000007A006B1000266666666666666620001B600A7000000 -000003C202B5000011111111111111100005B202C3000000 -00000098005B20000000000000000000002B500890000000 -0000003C3019910000000000000000000199103C30000000 -00000008A102B810000000000000000018B201A800000000 -00000002B7002B9200000000000000029B2007B200000000 -000000004C50029B510000000000015B92005C4000000000 -0000000005C50015BB52000000025BB51005C50000000000 -00000000005C710026BCB87678BCB620017C500000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000000028C82000000000000028C820000000000000 -0000000000000039CA53100000135AC93000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evBoundCancel [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -0000000000000039CA53100000135AC93000000000000000 -00000000000028C82000000000000028C820000000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000005C710026BCB87678BCB620017C500000000000 -0000000005C50015BB52000000025BB51005C50000000000 -000000004C50029B510000000000015B92005C4000000000 -00000002B7002B9200000000000000029B2007B200000000 -00000008A102B810000000000000000018B201A800000000 -0000003C3019910000000000000000000199103C30000000 -00000098005B20000000000000000000002B500890000000 -000003C202B5000001000000000001000005B202C3000000 -000007A006B1000016300000000036100001B600A7000000 -00000B501B500000036300000003630000005B105B000000 -00002C303C200000003630000036300000002C303C200000 -00003B105B000000000363000363000000000B501B300000 -00005B00780000000000363036300000000008700B500000 -00006A00970000000000036463000000000007900A600000 -000069009600000000000048400000000000069009600000 -00006A00970000000000036463000000000007900A600000 -00005B00780000000000363036300000000008700B500000 -00003B105B000000000363000363000000000B501B300000 -00002C303C200000003630000036300000002C303C200000 -00000B501B500000036300000003630000005B105B000000 -000007A006B1000016300000000036100001B600A7000000 -000003C202B5000001000000000001000005B202C3000000 -00000098005B20000000000000000000002B500890000000 -0000003C3019910000000000000000000199103C30000000 -00000008A102B810000000000000000018B201A800000000 -00000002B7002B9200000000000000029B2007B200000000 -000000004C50029B510000000000015B92005C4000000000 -0000000005C50015BB52000000025BB51005C50000000000 -00000000005C710026BCB87678BCB620017C500000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000000028C82000000000000028C820000000000000 -0000000000000039CA53100000135AC93000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evBoundComp [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -0000000000000039CA53100000135AC93000000000000000 -00000000000028C82000000000000028C820000000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000005C710026BCB87678BCB620017C500000000000 -0000000005C50015BB52000000025BB51005C50000000000 -000000004C50029B510000000000015B92005C4000000000 -00000002B7002B9200000000000000029B2007B200000000 -00000008A102B810000000000000000018B201A800000000 -0000003C3019910000000000000000000199103C30000000 -00000098005B20000000000000000000002B500890000000 -000003C202B5000100000001000000000005B202C3000000 -000007A006B1000541000026200000000001B600A7000000 -00000B501B500005551000266300000000005B105B000000 -00002C303C200005346200252640000000002C303C200000 -00003B105B000005203630250155100000000B501B300000 -00005B00780000052002543500145200000008700B500000 -00006A00970000052000155500003630000007900A600000 -000069009600000520000155000003610000069009600000 -00006A00970000052000155500003630000007900A600000 -00005B00780000052002543500145200000008700B500000 -00003B105B000005203630250155100000000B501B300000 -00002C303C200005346200252640000000002C303C200000 -00000B501B500005551000266300000000005B105B000000 -000007A006B1000541000026200000000001B600A7000000 -000003C202B5000100000001000000000005B202C3000000 -00000098005B20000000000000000000002B500890000000 -0000003C3019910000000000000000000199103C30000000 -00000008A102B810000000000000000018B201A800000000 -00000002B7002B9200000000000000029B2007B200000000 -000000004C50029B510000000000015B92005C4000000000 -0000000005C50015BB52000000025BB51005C50000000000 -00000000005C710026BCB87678BCB620017C500000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000000028C82000000000000028C820000000000000 -0000000000000039CA53100000135AC93000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evBoundCond [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -0000000000000039CA53100000135AC93000000000000000 -00000000000028C82000000000000028C820000000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000005C710026BCB87678BCB620017C500000000000 -0000000005C50015BB52000000025BB51005C50000000000 -000000004C50029B510000000000015B92005C4000000000 -00000002B7002B9200000000000000029B2007B200000000 -00000008A102B810000000000000000018B201A800000000 -0000003C3019910004888888888884000199103C30000000 -00000098005B20004BFFFFFFFFFFFB40002B500890000000 -000003C202B500008FB888888888BF800005B202C3000000 -000007A006B100008F80000000008F800001B600A7000000 -00000B501B5000008F80000000008F8000005B105B000000 -00002C303C2000008F80000000008F8000002C303C200000 -00003B105B0000008F82555555528F8000000B501B300000 -00005B00780000008F80000000008F80000008700B500000 -00006A00970000008F80000000008F80000007900A600000 -00006900960000008F82555555528F800000069009600000 -00006A00970000008F80000000008F80000007900A600000 -00005B00780000008F80000000008F80000008700B500000 -00003B105B0000008F82555555528F8000000B501B300000 -00002C303C2000008F80000000008F8000002C303C200000 -00000B501B5000008F80000000008F8000005B105B000000 -000007A006B100008F80000000008F800001B600A7000000 -000003C202B500008FB888888888BF800005B202C3000000 -00000098005B20004BFFFFFFFFFFFB40002B500890000000 -0000003C3019910004888888888884000199103C30000000 -00000008A102B810000000000000000018B201A800000000 -00000002B7002B9200000000000000029B2007B200000000 -000000004C50029B510000000000015B92005C4000000000 -0000000005C50015BB52000000025BB51005C50000000000 -00000000005C710026BCB87678BCB620017C500000000000 -000000000004BA3000135799975310003AB4000000000000 -00000000000028C82000000000000028C820000000000000 -0000000000000039CA53100000135AC93000000000000000 -000000000000000037BCBBA9ABBCB7300000000000000000 -000000000000000000023566653200000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evBoundMsgNI [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000003566653000000000000000000000 -000000000000000001116BA9AB6111000000000000000000 -000000000000000111000000000001110000000000000000 -000000000000284100000000000000014820000000000000 -000000000004BA2000000799970000002AB4000000000000 -00000000005C71000011187678111000017C500000000000 -0000000005C5001511000000000001151005C50000000000 -000000000950029B300000000000003B9200590000000000 -0000000011002B9200000000000000029B20011000000000 -000000011000B810000000000000000018B0001100000000 -000000010001310000000000000000000131000100000000 -000000110001000000000000000000000001000110000000 -000000100010000000000000000000000000100010000000 -000007800010000888888888888888880000100087000000 -00000B501620008FFFFFFFFFFFFFFFFF800026105B000000 -00002C303C20008FDA88888888888ADF80002C303C200000 -00003B105B00008F854000000000458F80000B501B300000 -00005B007800008F815510000015518F800008700B500000 -00006A009700008F800451000154008F800007900A600000 -000035005300008F800036202630008F8000035005300000 -000001001100008F800002646200008F8000011001000000 -000001001100008F800000252000008F8000011001000000 -000001000100008F800000000000008F8000010001000000 -000001000100008FB8888888888888BF8000010001000000 -000001000540008FFFFFFFFFFFFFFFFF8000450001000000 -0000013006B1000888888888888888880001B60031000000 -000003C202B5000000000000000000000005B202C3000000 -00000098005B20000000000000000000002B500890000000 -0000003C3019700000000000000000000079103C30000000 -00000008A10111000000000000000000011101A800000000 -00000002B70001100000000000000000110007B200000000 -000000004400001121000000000001211000044000000000 -0000000000100000BB52000000025BB00000100000000000 -000000000001100026BCB11111BCB6200011000000000000 -000000000000111000135111115310001110000000000000 -000000000000019820000000000000289100000000000000 -0000000000000039CA53100000135AC93000000000000000 -000000000000000037BC6111116CB7300000000000000000 -000000000000000000020001000200000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evBoundTimerNI [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000003566653000000000000000000000 -000000000000000001116BA9AB6111000000000000000000 -000000000000000111000000000001110000000000000000 -000000000000284100000000000000014820000000000000 -000000000004BA2000000799970000002AB4000000000000 -00000000005C71000011187678111000017C500000000000 -0000000005C5001511000000000001151005C50000000000 -000000000950029B300000000000003B9200590000000000 -0000000011002B9200000000000000029B20011000000000 -000000011000B810000000000000000018B0001100000000 -000000010001310000000000000000000131000100000000 -000000110001000000000133310000000001000110000000 -000000100010000000039BBBBB9300000000100010000000 -0000078000100000006C8312138C60000000100087000000 -00000B501620000006B400151004B600000026105B000000 -00002C303C2000003C40001510004C3000002C303C200000 -00003B105B000000980000151000089000000B501B300000 -00005B0078000001B3000015100003B1000008700B500000 -00006A0097000003B1000015111001B3000007900A600000 -0000350053000003B0000015555300B30000035005300000 -0000010011000003B1000001111001B30000011001000000 -0000010011000001B3000000000003B10000011001000000 -000001000100000098000000000008900000010001000000 -00000100010000003C40000000004C300000010001000000 -000001000540000006B400000004B6000000450001000000 -0000013006B10000006C8310138C60000001B60031000000 -000003C202B5000000039BBBBB9300000005B202C3000000 -00000098005B20000000013331000000002B500890000000 -0000003C3019700000000000000000000079103C30000000 -00000008A10111000000000000000000011101A800000000 -00000002B70001100000000000000000110007B200000000 -000000004400001121000000000001211000044000000000 -0000000000100000BB52000000025BB00000100000000000 -000000000001100026BCB11111BCB6200011000000000000 -000000000000111000135111115310001110000000000000 -000000000000019820000000000000289100000000000000 -0000000000000039CA53100000135AC93000000000000000 -000000000000000037BC6111116CB7300000000000000000 -000000000000000000020001000200000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evBoundEscNI [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000003566653000000000000000000000 -000000000000000001116BA9AB6111000000000000000000 -000000000000000111000000000001110000000000000000 -000000000000284100000000000000014820000000000000 -000000000004BA2000000799970000002AB4000000000000 -00000000005C71000011187678111000017C500000000000 -0000000005C5001511000000000001151005C50000000000 -000000000950029B300000000000003B9200590000000000 -0000000011002B9200000000000000029B20011000000000 -000000011000B810000000000000000018B0001100000000 -000000010001310000000000000000000131000100000000 -000000110001000000000000000000000001000110000000 -000000100010000000000000000000000000100010000000 -000007800010000000000000000000000000100087000000 -00000B50162000000000000000000000000026105B000000 -00002C303C200000000000000000000000002C303C200000 -00003B105B000000000000010000000000000B501B300000 -00005B00780000000000002620000000000008700B500000 -00006A00970000000000026662000000000007900A600000 -000035005300000000002636362000000000035005300000 -000001001100000000026216126200000000011001000000 -000001001100000000262016102620000000011001000000 -000001000100000002631126211362000000010001000000 -000001000100000016666666666666100000010001000000 -000001000540000001111126211111000000450001000000 -0000013006B1000000000016100000000001B60031000000 -000003C202B5000000000015100000000005B202C3000000 -00000098005B20000000000100000000002B500890000000 -0000003C3019700000000000000000000079103C30000000 -00000008A10111000000000000000000011101A800000000 -00000002B70001100000000000000000110007B200000000 -000000004400001121000000000001211000044000000000 -0000000000100000BB52000000025BB00000100000000000 -000000000001100026BCB11111BCB6200011000000000000 -000000000000111000135111115310001110000000000000 -000000000000019820000000000000289100000000000000 -0000000000000039CA53100000135AC93000000000000000 -000000000000000037BC6111116CB7300000000000000000 -000000000000000000020001000200000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evBoundSignalNI [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000003566653000000000000000000000 -000000000000000001116BA9AB6111000000000000000000 -000000000000000111000000000001110000000000000000 -000000000000284100000000000000014820000000000000 -000000000004BA2000000799970000002AB4000000000000 -00000000005C71000011187678111000017C500000000000 -0000000005C5001511000000000001151005C50000000000 -000000000950029B300000000000003B9200590000000000 -0000000011002B9200000000000000029B20011000000000 -000000011000B810000000000000000018B0001100000000 -000000010001310000000000000000000131000100000000 -000000110001000000000003000000000001000110000000 -000000100010000000000037300000000000100010000000 -000007800010000000000164610000000000100087000000 -00000B50162000000000035153000000000026105B000000 -00002C303C200000000016202610000000002C303C200000 -00003B105B000000000045000540000000000B501B300000 -00005B00780000000001620002610000000008700B500000 -00006A00970000000004500000540000000007900A600000 -000035005300000000262000002620000000035005300000 -000001001100000000540000000450000000011001000000 -000001001100000002610000000162000000011001000000 -000001000100000005400000000045000000010001000000 -000001000100000026100000000016200000010001000000 -000001000540000054111111111114500000450001000000 -0000013006B1000266666666666666620001B60031000000 -000003C202B5000011111111111111100005B202C3000000 -00000098005B20000000000000000000002B500890000000 -0000003C3019700000000000000000000079103C30000000 -00000008A10111000000000000000000011101A800000000 -00000002B70001100000000000000000110007B200000000 -000000004400001121000000000001211000044000000000 -0000000000100000BB52000000025BB00000100000000000 -000000000001100026BCB11111BCB6200011000000000000 -000000000000111000135111115310001110000000000000 -000000000000019820000000000000289100000000000000 -0000000000000039CA53100000135AC93000000000000000 -000000000000000037BC6111116CB7300000000000000000 -000000000000000000020001000200000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $evBoundCondNI [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000003566653000000000000000000000 -000000000000000001116BA9AB6111000000000000000000 -000000000000000111000000000001110000000000000000 -000000000000284100000000000000014820000000000000 -000000000004BA2000000799970000002AB4000000000000 -00000000005C71000011187678111000017C500000000000 -0000000005C5001511000000000001151005C50000000000 -000000000950029B300000000000003B9200590000000000 -0000000011002B9200000000000000029B20011000000000 -000000011000B810000000000000000018B0001100000000 -000000010001310004888888888884000131000100000000 -00000011000100004BFFFFFFFFFFFB400001000110000000 -00000010001000008FB888888888BF800000100010000000 -00000780001000008F80000000008F800000100087000000 -00000B50162000008F80000000008F80000026105B000000 -00002C303C2000008F80000000008F8000002C303C200000 -00003B105B0000008F82555555528F8000000B501B300000 -00005B00780000008F80000000008F80000008700B500000 -00006A00970000008F80000000008F80000007900A600000 -00003500530000008F82555555528F800000035005300000 -00000100110000008F80000000008F800000011001000000 -00000100110000008F80000000008F800000011001000000 -00000100010000008F82555555528F800000010001000000 -00000100010000008F80000000008F800000010001000000 -00000100054000008F80000000008F800000450001000000 -0000013006B100008F80000000008F800001B60031000000 -000003C202B500008FB888888888BF800005B202C3000000 -00000098005B20004BFFFFFFFFFFFB40002B500890000000 -0000003C3019700004888888888884000079103C30000000 -00000008A10111000000000000000000011101A800000000 -00000002B70001100000000000000000110007B200000000 -000000004400001121000000000001211000044000000000 -0000000000100000BB52000000025BB00000100000000000 -000000000001100026BCB11111BCB6200011000000000000 -000000000000111000135111115310001110000000000000 -000000000000019820000000000000289100000000000000 -0000000000000039CA53100000135AC93000000000000000 -000000000000000037BC6111116CB7300000000000000000 -000000000000000000020001000200000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $gwXor [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000001000000000000000000000000 -00000000000000000000004C400000000000000000000000 -0000000000000000000004C6C40000000000000000000000 -000000000000000000004C404C4000000000000000000000 -00000000000000000004C40004C400000000000000000000 -0000000000000000004C4000004C40000000000000000000 -000000000000000004C400000004C4000000000000000000 -00000000000000004C40000000004C400000000000000000 -0000000000000004C4000000000004C40000000000000000 -000000000000004C400000000000004C4000000000000000 -00000000000004C40000000000000004C400000000000000 -0000000000004C4000000000000000004C40000000000000 -000000000004C400000000000000000004C4000000000000 -00000000004C40010000000000000001004C400000000000 -0000000004C4001630000000000000361004C40000000000 -000000004C400003630000000000036300004C4000000000 -00000004C40000003630000000003630000004C400000000 -0000004C4000000003630000000363000000004C40000000 -000004C400000000003630000036300000000004C4000000 -00004C40000000000003630003630000000000004C400000 -0004C4000000000000003631363000000000000004C40000 -004C400000000000000003646300000000000000004C4000 -01C60000000000000000014841000000000000000006C100 -004C400000000000000003646300000000000000004C4000 -0004C4000000000000003631363000000000000004C40000 -00004C40000000000003630003630000000000004C400000 -000004C400000000003630000036300000000004C4000000 -0000004C4000000003630000000363000000004C40000000 -00000004C40000003630000000003630000004C400000000 -000000004C400003630000000000036300004C4000000000 -0000000004C4001630000000000000361004C40000000000 -00000000004C40010000000000000001004C400000000000 -000000000004C400000000000000000004C4000000000000 -0000000000004C4000000000000000004C40000000000000 -00000000000004C40000000000000004C400000000000000 -000000000000004C400000000000004C4000000000000000 -0000000000000004C4000000000004C40000000000000000 -00000000000000004C40000000004C400000000000000000 -000000000000000004C400000004C4000000000000000000 -0000000000000000004C4000004C40000000000000000000 -00000000000000000004C40004C400000000000000000000 -000000000000000000004C404C4000000000000000000000 -0000000000000000000004C6C40000000000000000000000 -00000000000000000000004C400000000000000000000000 -000000000000000000000001000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $gwAnd [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000001000000000000000000000000 -00000000000000000000004C400000000000000000000000 -0000000000000000000004C6C40000000000000000000000 -000000000000000000004C404C4000000000000000000000 -00000000000000000004C40004C400000000000000000000 -0000000000000000004C4000004C40000000000000000000 -000000000000000004C400000004C4000000000000000000 -00000000000000004C40000000004C400000000000000000 -0000000000000004C4000000000004C40000000000000000 -000000000000004C400000000000004C4000000000000000 -00000000000004C40000000000000004C400000000000000 -0000000000004C4000000000000000004C40000000000000 -000000000004C400000000020000000004C4000000000000 -00000000004C40000000002620000000004C400000000000 -0000000004C4000000000026200000000004C40000000000 -000000004C400000000000262000000000004C4000000000 -00000004C40000000000002620000000000004C400000000 -0000004C4000000000000026200000000000004C40000000 -000004C400000000000000262000000000000004C4000000 -00004C40000000000000002620000000000000004C400000 -0004C4000000000000000026200000000000000004C40000 -004C400000000022222222262222222220000000004C4000 -01C60000000002666666666666666666620000000006C100 -004C400000000022222222262222222220000000004C4000 -0004C4000000000000000026200000000000000004C40000 -00004C40000000000000002620000000000000004C400000 -000004C400000000000000262000000000000004C4000000 -0000004C4000000000000026200000000000004C40000000 -00000004C40000000000002620000000000004C400000000 -000000004C400000000000262000000000004C4000000000 -0000000004C4000000000026200000000004C40000000000 -00000000004C40000000002620000000004C400000000000 -000000000004C400000000020000000004C4000000000000 -0000000000004C4000000000000000004C40000000000000 -00000000000004C40000000000000004C400000000000000 -000000000000004C400000000000004C4000000000000000 -0000000000000004C4000000000004C40000000000000000 -00000000000000004C40000000004C400000000000000000 -000000000000000004C400000004C4000000000000000000 -0000000000000000004C4000004C40000000000000000000 -00000000000000000004C40004C400000000000000000000 -000000000000000000004C404C4000000000000000000000 -0000000000000000000004C6C40000000000000000000000 -00000000000000000000004C400000000000000000000000 -000000000000000000000001000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $gwOr [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000001000000000000000000000000 -00000000000000000000004C400000000000000000000000 -0000000000000000000004C6C40000000000000000000000 -000000000000000000004C404C4000000000000000000000 -00000000000000000004C40004C400000000000000000000 -0000000000000000004C4000004C40000000000000000000 -000000000000000004C400000004C4000000000000000000 -00000000000000004C40000000004C400000000000000000 -0000000000000004C4000000000004C40000000000000000 -000000000000004C400000000000004C4000000000000000 -00000000000004C40000000000000004C400000000000000 -0000000000004C4000000000000000004C40000000000000 -000000000004C400000000000000000004C4000000000000 -00000000004C40000000000000000000004C400000000000 -0000000004C4000000000123210000000004C40000000000 -000000004C40000000027BCCCB72000000004C4000000000 -00000004C4000000004BB63336BB4000000004C400000000 -0000004C4000000004C710000017C4000000004C40000000 -000004C4000000002B71000000017B2000000004C4000000 -00004C40000000007B10000000001B70000000004C400000 -0004C40000000001B6000000000006B10000000004C40000 -004C400000000002C3000000000003C200000000004C4000 -01C6000000000003C3000000000003C3000000000006C100 -004C400000000002C3000000000003C200000000004C4000 -0004C40000000001B6000000000006B10000000004C40000 -00004C40000000007B10000000001B70000000004C400000 -000004C4000000002B71000000017B2000000004C4000000 -0000004C4000000004C710000017C4000000004C40000000 -00000004C4000000004BB63336BB4000000004C400000000 -000000004C40000000027BCCCB72000000004C4000000000 -0000000004C4000000000123210000000004C40000000000 -00000000004C40000000000000000000004C400000000000 -000000000004C400000000000000000004C4000000000000 -0000000000004C4000000000000000004C40000000000000 -00000000000004C40000000000000004C400000000000000 -000000000000004C400000000000004C4000000000000000 -0000000000000004C4000000000004C40000000000000000 -00000000000000004C40000000004C400000000000000000 -000000000000000004C400000004C4000000000000000000 -0000000000000000004C4000004C40000000000000000000 -00000000000000000004C40004C400000000000000000000 -000000000000000000004C404C4000000000000000000000 -0000000000000000000004C6C40000000000000000000000 -00000000000000000000004C400000000000000000000000 -000000000000000000000001000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $gwComplex [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000001000000000000000000000000 -00000000000000000000004C400000000000000000000000 -0000000000000000000004C6C40000000000000000000000 -000000000000000000004C404C4000000000000000000000 -00000000000000000004C40004C400000000000000000000 -0000000000000000004C4000004C40000000000000000000 -000000000000000004C400000004C4000000000000000000 -00000000000000004C40000000004C400000000000000000 -0000000000000004C4000000000004C40000000000000000 -000000000000004C400000000000004C4000000000000000 -00000000000004C40000000000000004C400000000000000 -0000000000004C4000000000000000004C40000000000000 -000000000004C400000000000000000004C4000000000000 -00000000004C40000000000000000000004C400000000000 -0000000004C4000000000001000000000004C40000000000 -000000004C400000000000151000000000004C4000000000 -00000004C40000000010001610001000000004C400000000 -0000004C4000000001520016100251000000004C40000000 -000004C400000000002620161026200000000004C4000000 -00004C40000000000002621612620000000000004C400000 -0004C4000000000000002636362000000000000004C40000 -004C400000000000111113666311111000000000004C4000 -01C60000000000015666666866666651000000000006C100 -004C400000000000111113666311111000000000004C4000 -0004C4000000000000002636362000000000000004C40000 -00004C40000000000002621612620000000000004C400000 -000004C400000000002620161026200000000004C4000000 -0000004C4000000001520016100251000000004C40000000 -00000004C40000000010001610001000000004C400000000 -000000004C400000000000151000000000004C4000000000 -0000000004C4000000000001000000000004C40000000000 -00000000004C40000000000000000000004C400000000000 -000000000004C400000000000000000004C4000000000000 -0000000000004C4000000000000000004C40000000000000 -00000000000004C40000000000000004C400000000000000 -000000000000004C400000000000004C4000000000000000 -0000000000000004C4000000000004C40000000000000000 -00000000000000004C40000000004C400000000000000000 -000000000000000004C400000004C4000000000000000000 -0000000000000000004C4000004C40000000000000000000 -00000000000000000004C40004C400000000000000000000 -000000000000000000004C404C4000000000000000000000 -0000000000000000000004C6C40000000000000000000000 -00000000000000000000004C400000000000000000000000 -000000000000000000000001000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $gwEvent [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000001000000000000000000000000 -00000000000000000000004C400000000000000000000000 -0000000000000000000004C6C40000000000000000000000 -000000000000000000004C404C4000000000000000000000 -00000000000000000004C40004C400000000000000000000 -0000000000000000004C4000004C40000000000000000000 -000000000000000004C400000004C4000000000000000000 -00000000000000004C40000000004C400000000000000000 -0000000000000004C4000000000004C40000000000000000 -000000000000004C400000000000004C4000000000000000 -00000000000004C40000000000000004C400000000000000 -0000000000004C4000002467642000004C40000000000000 -000000000004C400014ABA878ABA410004C4000000000000 -00000000004C40002AB510000015BA20004C400000000000 -0000000004C40003B7100013100017B30004C40000000000 -000000004C40002B600001565100006B20004C4000000000 -00000004C40001A70000363136300007A10004C400000000 -0000004C400004B10014620002641001B400004C40000000 -000004C400000A5002551000001552005A000004C4000000 -00004C4000002B1036300000000036301B2000004C400000 -0004C40000004A0162000000000002610A40000004C40000 -004C400000006800530000000000035008600000004C4000 -01C60000000077002500000000000520077000000006C100 -004C400000006800161000000000161008600000004C4000 -0004C40000004A0005300000000035000A40000004C40000 -00004C4000002B1003500000000053001B2000004C400000 -000004C400000A5001610000000161005A000004C4000000 -0000004C400004B10053111111135001B400004C40000000 -00000004C40001A70036666666663007A10004C400000000 -000000004C40002B600111111111006B20004C4000000000 -0000000004C40003B7100000000017B30004C40000000000 -00000000004C40002AB510000015BA20004C400000000000 -000000000004C400014ABA878ABA410004C4000000000000 -0000000000004C4000002467642000004C40000000000000 -00000000000004C40000000000000004C400000000000000 -000000000000004C400000000000004C4000000000000000 -0000000000000004C4000000000004C40000000000000000 -00000000000000004C40000000004C400000000000000000 -000000000000000004C400000004C4000000000000000000 -0000000000000000004C4000004C40000000000000000000 -00000000000000000004C40004C400000000000000000000 -000000000000000000004C404C4000000000000000000000 -0000000000000000000004C6C40000000000000000000000 -00000000000000000000004C400000000000000000000000 -000000000000000000000001000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $gwParEvent [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000001000000000000000000000000 -00000000000000000000004C400000000000000000000000 -0000000000000000000004C6C40000000000000000000000 -000000000000000000004C404C4000000000000000000000 -00000000000000000004C40004C400000000000000000000 -0000000000000000004C4000004C40000000000000000000 -000000000000000004C400000004C4000000000000000000 -00000000000000004C40000000004C400000000000000000 -0000000000000004C4000000000004C40000000000000000 -000000000000004C400000000000004C4000000000000000 -00000000000004C40000000000000004C400000000000000 -0000000000004C4000000000000000004C40000000000000 -000000000004C400000000000000000004C4000000000000 -00000000004C40000000000000000000004C400000000000 -0000000004C4000000001356531000000004C40000000000 -000000004C4000000015BBA9ABB5100000004C4000000000 -00000004C4000000019B5101015B9100000004C400000000 -0000004C4000000019A200151002A9100000004C40000000 -000004C4000000005B20001610002B5000000004C4000000 -00004C4000000001B5000016100005B1000000004C400000 -0004C40000000003B1000016100001B30000000004C40000 -004C400000000005A0111126211110A500000000004C4000 -01C60000000000069156666666665196000000000006C100 -004C400000000005A0111126211110A500000000004C4000 -0004C40000000003B1000016100001B30000000004C40000 -00004C4000000001B5000016100005B1000000004C400000 -000004C4000000005B20001610002B5000000004C4000000 -0000004C4000000019A200151002A9100000004C40000000 -00000004C4000000019B5101015B9100000004C400000000 -000000004C4000000015BBA9ABB5100000004C4000000000 -0000000004C4000000001356531000000004C40000000000 -00000000004C40000000000000000000004C400000000000 -000000000004C400000000000000000004C4000000000000 -0000000000004C4000000000000000004C40000000000000 -00000000000004C40000000000000004C400000000000000 -000000000000004C400000000000004C4000000000000000 -0000000000000004C4000000000004C40000000000000000 -00000000000000004C40000000004C400000000000000000 -000000000000000004C400000004C4000000000000000000 -0000000000000000004C4000004C40000000000000000000 -00000000000000000004C40004C400000000000000000000 -000000000000000000004C404C4000000000000000000000 -0000000000000000000004C6C40000000000000000000000 -00000000000000000000004C400000000000000000000000 -000000000000000000000001000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $icUser [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000034300000000000000000000000 -0000000000000000000019CEC91000000000000000000000 -000000000000000000008DFFFD8000000000000000000000 -00000000000000000001BFFFFFB100000000000000000000 -00000000000000000002CFFFFFC200000000000000000000 -00000000000000000001BEFFFEB100000000000000000000 -000000000000000000005DEFED5000000000000000000000 -0000000000000000000004ABA40000000000000000000000 -000000000000000000030000000300000000000000000000 -000000000000000000260000000620000000000000000000 -000000000000000000440000000440000000000000000000 -000000000000000001610000000161000000000000000000 -000000000000000002500000000052000000000000000000 -000000000000000005311111111135000000000000000000 -000000000000000016666666666666100000000000000000 -000000000000000001111111111111000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $icService [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000003000000000000000000000000 -000000000000000000000016100000000000000000000000 -000000000000000001200016100021000000000000000000 -000000000000000002620147410262000000000000000000 -000000000000000000265BCCCB5620000000000000000000 -00000000000000000005C73537C500000000000000000000 -0000000000000000001B7400047B10000000000000000000 -0000000000000000114C3000003C41100000000000000000 -0000000000000003667C5000005C76630000000000000000 -0000000000000000114C3000003C41100000000000000000 -0000000000000000001B7400047B10000000000000000000 -00000000000000000005C73537C500000000000000000000 -000000000000000000265BCCCB5620000000000000000000 -000000000000000002620147410262000000000000000000 -000000000000000001200016100021000000000000000000 -000000000000000000000016100000000000000000000000 -000000000000000000000003000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $icScript [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000004888888888884000000000000000000 -00000000000000004BFFFFFFFFFFFB400000000000000000 -00000000000000008FB888888888BF800000000000000000 -00000000000000008F80000000008F800000000000000000 -00000000000000008F80000000008F800000000000000000 -00000000000000008F80000000008F800000000000000000 -00000000000000008F82555555528F800000000000000000 -00000000000000008F80000000008F800000000000000000 -00000000000000008F80000000008F800000000000000000 -00000000000000008F82555555528F800000000000000000 -00000000000000008F80000000008F800000000000000000 -00000000000000008F80000000008F800000000000000000 -00000000000000008F82555555528F800000000000000000 -00000000000000008F80000000008F800000000000000000 -00000000000000008F80000000008F800000000000000000 -00000000000000008F80000000008F800000000000000000 -00000000000000008FB888888888BF800000000000000000 -00000000000000004BFFFFFFFFFFFB400000000000000000 -000000000000000004888888888884000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $icManual [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000010000000000000000000000000 -000000000000000000000151003000000000000000000000 -000000000000000000100160016100000000000000000000 -000000000000000001510260026000100000000000000000 -000000000000000003500350035001510000000000000000 -000000000000000005300440044002600000000000000000 -000000000000000016100530053003500000000000000000 -000000000000000035000620052005300000000000000000 -000000000000000053112621262116200000000000000000 -000000000000000166666666666666610000000000000000 -000000000000000011111111111111100000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $icRule [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000011111111111111100000000000000000 -000000000000000156666666666666510000000000000000 -000000000000000161111116111111610000000000000000 -000000000000000161000016100001610000000000000000 -000000000000000161000016100001610000000000000000 -000000000000000161000016100001610000000000000000 -000000000000000161111116111111610000000000000000 -000000000000000166666666666666610000000000000000 -000000000000000161111116111111610000000000000000 -000000000000000161000016100001610000000000000000 -000000000000000161000016100001610000000000000000 -000000000000000161000016100001610000000000000000 -000000000000000161111116111111610000000000000000 -000000000000000156666666666666510000000000000000 -000000000000000011111111111111100000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $icPlusbox [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000048888888888888400000000000000000 -0000000000000004BFFFFFFFFFFFFFB40000000000000000 -0000000000000008FB88888888888BF80000000000000000 -0000000000000008F8000003000008F80000000000000000 -0000000000000008F8000016100008F80000000000000000 -0000000000000008F8000016100008F80000000000000000 -0000000000000008F8000016100008F80000000000000000 -0000000000000008F8011126211108F80000000000000000 -0000000000000008F8366666666638F80000000000000000 -0000000000000008F8011126211108F80000000000000000 -0000000000000008F8000016100008F80000000000000000 -0000000000000008F8000016100008F80000000000000000 -0000000000000008F8000016100008F80000000000000000 -0000000000000008F8000003000008F80000000000000000 -0000000000000008FB88888888888BF80000000000000000 -0000000000000004BFFFFFFFFFFFFFB40000000000000000 -000000000000000048888888888888400000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $icMsg [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000888888888888888880000000000000000 -000000000000008FFFFFFFFFFFFFFFFF8000000000000000 -000000000000008FDA88888888888ADF8000000000000000 -000000000000008F854000000000458F8000000000000000 -000000000000008F815510000015518F8000000000000000 -000000000000008F800451000154008F8000000000000000 -000000000000008F800036202630008F8000000000000000 -000000000000008F800002646200008F8000000000000000 -000000000000008F800000252000008F8000000000000000 -000000000000008F800000000000008F8000000000000000 -000000000000008FB8888888888888BF8000000000000000 -000000000000008FFFFFFFFFFFFFFFFF8000000000000000 -000000000000000888888888888888880000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $icMsgf [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000888888888888888880000000000000000 -000000000000008FFFFFFFFFFFFFFFFF8000000000000000 -000000000000008FFFFFFFFFFFFFFFFF8000000000000000 -000000000000008FFFFFFFFFFFFFFFFF8000000000000000 -000000000000008FFFFFFFFFFFFFFFFF8000000000000000 -000000000000008FFFFFFFFFFFFFFFFF8000000000000000 -000000000000008FFFFFFFFFFFFFFFFF8000000000000000 -000000000000008FFFFFFFFFFFFFFFFF8000000000000000 -000000000000008FFFFFFFFFFFFFFFFF8000000000000000 -000000000000008FFFFFFFFFFFFFFFFF8000000000000000 -000000000000008FFFFFFFFFFFFFFFFF8000000000000000 -000000000000008FFFFFFFFFFFFFFFFF8000000000000000 -000000000000000888888888888888880000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} - -sprite $icLoop [48x48/16] { -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000002578752000000000000000000000 -00000000000000000039CB989BC930000000000000000000 -000000000000000005C931000139C5000000000000000000 -00000000000000005D61000000016D500000000000000000 -0000000000000003C6000000000006C30000000000000000 -000000000000000991000000000001990000000000000000 -000000000000002C300000000000003C2000000000000000 -000000000000005B100000000000001B5000000000000000 -000000000000007900000000000000097000000000000000 -000000000000008800000000000000088000000000000000 -000000000000007900000000000000097000000000000000 -000000000000005B100000000000011B5000000000000000 -000000000000002C300000000000265C2000000000000000 -000000000000000991000000000264A90000000000000000 -0000000000000003C6000000002636C62000000000000000 -00000000000000005D61000002636D526200000000000000 -000000000000000005C931001659C5002610000000000000 -00000000000000000039CB989BC930000100000000000000 -000000000000000000002578752000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000 -} diff --git a/_method_extract.txt b/_method_extract.txt new file mode 100644 index 0000000..aa2767a --- /dev/null +++ b/_method_extract.txt @@ -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 +СВЕДЕНИЯ ОБ АВТОРАХ + +Бергер Екатерина Геннадьевна, к.э.н., доцент кафедры практической и при- +кладной информатики Института информационных технологий +Кириллина Юлия Владимировна, к.э.н., доцент, доцент кафедры практиче- +ской и прикладной информатики Института информационных технологий +Лагунова Анна Дмитриевна, к.э.н., доцент кафедры практической и при- +кладной информатики Института информационных технологий + \ No newline at end of file diff --git a/build-exe-fast.bat b/build-exe-fast.bat index d911526..3cfcd8b 100644 --- a/build-exe-fast.bat +++ b/build-exe-fast.bat @@ -13,34 +13,37 @@ 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. -where python >nul 2>&1 +call "%~dp0build-find-python311.bat" if errorlevel 1 goto :nopython - -python -c "import sys; raise SystemExit(sys.hexversion < 0x030A0000)" -if errorlevel 1 goto :oldpython +if not defined PYEXE goto :nopython echo [1/4] Зависимости ... -python -c "import PyInstaller, matplotlib, docx, marko, lxml, pygments, PIL, freetype, latex2mathml, requests, docxcompose" 1>nul 2>nul +"%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 ... -python -m pip install -q -e . "pyinstaller>=6.0" "matplotlib>=3.7" +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 + Mermaid assets ... -python scripts\fetch_plantuml.py +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" ( - python scripts\fetch_mermaid.py + "%PYEXE%" scripts\fetch_mermaid.py ) echo [3/4] PyInstaller onefile, кэш build-fast ... -python -m PyInstaller --noconfirm %PYI_CLEAN% --workpath build-fast --distpath dist md2gost.fast.spec +"%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 @@ -57,12 +60,7 @@ if "%DO_PAUSE%"=="1" pause exit /b 0 :nopython -echo Python не найден в PATH. Установите Python 3.10+ и отметьте "Add python.exe to PATH". -goto :fail - -:oldpython -echo Нужен Python 3.10 или новее. -python --version +echo Нужен именно Python 3.11. Через uv без системного 3.11: build-exe-uvx.bat goto :fail :pipfail diff --git a/build-exe-uvx.bat b/build-exe-uvx.bat new file mode 100644 index 0000000..7cf8b45 --- /dev/null +++ b/build-exe-uvx.bat @@ -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 diff --git a/build-exe.bat b/build-exe.bat index 40bacbc..4f11ba3 100644 --- a/build-exe.bat +++ b/build-exe.bat @@ -5,34 +5,37 @@ chcp 65001 >nul 2>&1 echo === md2gost: сборка exe === echo Каталог: %CD% +echo Python: 3.11 ^(ищем, не берём первый python из PATH^) echo. -where python >nul 2>&1 +call "%~dp0build-find-python311.bat" if errorlevel 1 goto :nopython - -python -c "import sys; raise SystemExit(sys.hexversion < 0x030A0000)" -if errorlevel 1 goto :oldpython +if not defined PYEXE goto :nopython echo [1/4] Зависимости проекта + PyInstaller + matplotlib ... -python -m pip install -q -e . "pyinstaller>=6.0" "matplotlib>=3.7" +"%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 + Mermaid assets ... -python scripts\fetch_plantuml.py +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" ( - python scripts\fetch_mermaid.py + "%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 подцепит консоль сам ... -python -m PyInstaller --noconfirm --clean md2gost.spec +"%PYEXE%" -m PyInstaller --noconfirm --clean md2gost.spec if errorlevel 1 goto :pyifail if not exist "dist\md2gost.exe" goto :noexe @@ -48,12 +51,7 @@ pause exit /b 0 :nopython -echo Python не найден в PATH. Установите Python 3.10+ и отметьте "Add python.exe to PATH". -goto :fail - -:oldpython -echo Нужен Python 3.10 или новее. -python --version +echo Нужен именно Python 3.11. Через uv без системного 3.11: build-exe-uvx.bat goto :fail :pipfail diff --git a/build-find-python311.bat b/build-find-python311.bat new file mode 100644 index 0000000..01fc8f7 --- /dev/null +++ b/build-find-python311.bat @@ -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 diff --git a/delete_this.txt b/delete_this.txt new file mode 100644 index 0000000..6990bea --- /dev/null +++ b/delete_this.txt @@ -0,0 +1,4 @@ +tests/_idef0_preview.png +tests/_idef0_preview.svg +tests/_idef0_preview_a0.png +tests/_idef0_preview_a0.svg diff --git a/docs/README.md b/docs/README.md index 41fc472..e63163d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,11 +6,14 @@ 1. [Быстрый старт](quickstart.md) — установка, первое окно, exe 2. [Синтаксис Markdown](markdown.md) — чем диалект отличается от обычного MD -3. [Схемы и диаграммы](schemes.md) — UML, Mermaid, настраиваемые схемы -4. [Типы документов](types.md) — coursework, practice, vkr, PIS_custom, APID_coursework -5. [CLI и GUI](cli-gui.md) — флаги и соответствие в интерфейсе -6. [Кастомные стили (JSON)](styles.md) — оверлей полей и стилей абзацев -7. [Промпты для ИИ](prompts.md) — генерация `.md`, расширение промпта схемами +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/). diff --git a/docs/archimate.md b/docs/archimate.md new file mode 100644 index 0000000..e52c9be --- /dev/null +++ b/docs/archimate.md @@ -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 <> +archimate #Application "Портал" as c <> +c --> p +@enduml +``` + +Sprites: `<<$archimate/business-process>>` или `jar:archimate/...`. +**Не смешивайте** макросы и сырой `archimate` на одной схеме. + +### 7.3. Темы + +```plantuml +!theme archimate-standard from +' 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 `) + +### Примеры композиции (не словарь языка) + +- 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`). diff --git a/docs/cli-gui.md b/docs/cli-gui.md index 76d957f..9fb69a3 100644 --- a/docs/cli-gui.md +++ b/docs/cli-gui.md @@ -8,7 +8,9 @@ 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`. @@ -24,6 +26,10 @@ python -m md2gost report.docx --check-pages # только проверка в | `--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` | @@ -34,6 +40,7 @@ python -m md2gost report.docx --check-pages # только проверка в | `--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) | @@ -47,14 +54,31 @@ DOCX не умеет сам писать «Продолжение Таблицы ## Меню GUI - **Настройки → Файлы** — шаблон, титул, задание, стили JSON, выходной путь -- **Настройки → Диаграммы** — PlantUML, Mermaid (Chromium), Kroki, формат, кэш includes +- **Настройки → Студент…** — ФИО и группа (при первом запуске спрашивают сами) +- **Настройки → Метаданные…** — свойства 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`. diff --git a/docs/markdown.md b/docs/markdown.md index 9cae72d..d349547 100644 --- a/docs/markdown.md +++ b/docs/markdown.md @@ -89,7 +89,7 @@ def f(): ## Диаграмма → Рисунок -Перед блоком — `%id Подпись`. Языки: `uml`, `plantuml`, `mermaid`/`mmd`, или id схемы (`c4`, `bpmn`, …). +Перед блоком — `%id Подпись`. Языки: `uml`, `plantuml`, `mermaid`/`mmd`, `idef0`, `dfd`/`data-flow-diagram` (Graphviz вшитый/PATH или Kroki), или id схемы (`c4`, `usecase`, …). - `+listing` — ещё и Листинг с исходником - `+landscape` — альбомная страница под широкий рисунок/таблицу @@ -144,6 +144,12 @@ $$ - Тире в предложениях: «—» (по умолчанию сохраняется). Замена на «-»: `--emdash-to-hyphen`. - Строка `---` / `***` / `___` по умолчанию **игнорируется**. Разрыв страницы: `--hr-pagebreak` или галочка в GUI. +## Автотитульник (метаданные в MD) + +В начале файла можно указать блок с оградой `title` (номер работы, год, оверрайды полей). Рядом с файлом — `info_conv.yaml` с институтом, кафедрой, дисциплиной и преподавателем. ФИО и группа студента задаются в GUI / CLI, не в yaml. Автогенерация титула **по умолчанию выкл.** — включите `--auto-title` или галочку «Автотитул». + +Подробно: [title.md](title.md). + ## Запрещено в отчёте - «рис.», «табл.» diff --git a/docs/prompts.md b/docs/prompts.md index 0713cd7..b6e97f3 100644 --- a/docs/prompts.md +++ b/docs/prompts.md @@ -9,11 +9,12 @@ | [`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). -2. В GUI включите нужные схемы (C4, BPMN, …) — в конец промпта добавятся макросы и `ai-prompt` из `md2gost.schemes.json`. +1. Откройте промпт (файл или GUI). При необходимости добавьте `emulate-student` после базового generate-*. +2. В GUI включите нужные схемы (C4, usecase, …) — в конец промпта добавятся макросы и `ai-prompt` из `md2gost.schemes.json`. 3. Скопируйте собранный текст в ChatGPT / Claude / Cursor / Copilot. 4. Следующим сообщением: тип работы, тема, черновик / требования. 5. Сохраните ответ как `.md` и конвертируйте: @@ -24,7 +25,7 @@ python -m md2gost report.md -o report.docx --type coursework --check --strict ## Зачем кнопки схем -Базовый промпт уже говорит: блоки `` ```uml `` / `` ```mermaid `` / `` ```uml-c4 `` конвертер сам превратит в Рисунок. Кнопки нужны, когда модели надо знать **конкретные макросы** выбранной схемы (Person/Rel для C4, Start/XOR/Flow для BPMN и т.д.) — в том числе пользовательских из «Шаблоны UML». +Базовый промпт уже говорит: блоки `` ```uml `` / `` ```mermaid `` / `` ```uml-c4 `` конвертер сам превратит в Рисунок. Кнопки нужны, когда модели надо знать **конкретные макросы** выбранной схемы (Person/Rel для C4, actor/usecase и т.д.) — в том числе пользовательских из «Шаблоны UML». ## Подробнее о синтаксисе diff --git a/docs/quickstart.md b/docs/quickstart.md index 65f5eb3..7467ce4 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -38,11 +38,13 @@ md2gost-gui # то же GUI (entry point) 1. Перетащите `.md` в верхнюю область (или кликните по ней). 2. Выберите тип документа (`practice` по умолчанию). -3. При необходимости: Настройки → Файлы (титул, задание), Настройки → Диаграммы, меню «Шаблоны UML». +3. При необходимости: Настройки → Файлы (титул, задание), **Настройки → Студент…** (ФИО и группа для автотитула), **Настройки → Метаданные…** (свойства DOCX), Настройки → Диаграммы, меню «Шаблоны UML». 4. Нажмите «Конвертировать». Справка в меню: **Инструкция**, **Схемы**, **Промпт для ИИ**. +Автотитул (`info_conv.yaml` + блок `title`): [title.md](title.md). + ## Типы документов (кратко) | `--type` | Назначение | @@ -57,5 +59,6 @@ md2gost-gui # то же GUI (entry point) ## Следующие шаги - Написать отчёт в диалекте md2gost: [markdown.md](markdown.md) +- Автотитульник: [title.md](title.md) - Вставить диаграммы: [schemes.md](schemes.md) - Сгенерировать `.md` через ИИ: [prompts.md](prompts.md) diff --git a/docs/schemes.md b/docs/schemes.md index 071a314..694a3e7 100644 --- a/docs/schemes.md +++ b/docs/schemes.md @@ -1,6 +1,6 @@ # Схемы и диаграммы -В отчёте блок кода с языком диаграммы превращается в **Рисунок** (PNG или SVG). PNG руками экспортировать не нужно: md2gost вызывает PlantUML (jar / Kroki) или локальный Mermaid (браузер / QuickJS), иначе Kroki. +В отчёте блок кода с языком диаграммы превращается в **Рисунок** (PNG или SVG). PNG руками экспортировать не нужно: md2gost вызывает PlantUML (jar / Kroki), локальный Mermaid (браузер / QuickJS), локальный IDEF0 (Pillow) или DFD через [data-flow-diagram](https://github.com/pbauermeister/dfd) (вшитый / системный Graphviz или Kroki). ## Синтаксис в markdown @@ -32,13 +32,12 @@ Rel(user, app, "логин") | `` ```uml `` / `` ```plantuml `` | Сырой PlantUML (`@startuml`…`@enduml`) | | `` ```uml- `` / `` ``` `` | Схема из `md2gost.schemes.json` (обёртка и `!include` добавляются сами) | | `` ```mermaid `` / `` ```mmd `` | Mermaid: браузер / QuickJS / Kroki | -| `` ```bpmn `` / `` ```uml-bpmn `` | BPMN 2.0 (макросы библиотеки md2gost) | - -**IDEF0 / DFD** конвертер не рисует — вставляйте готовый PNG как обычный Рисунок. +| `` ```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` рядом с приложением (существующий файл не перезаписывается). +Файл шаблона: [`md2gost/diagrams/schemes.json`](../md2gost/diagrams/schemes.json). При первом запуске копируется в `md2gost.schemes.json` рядом с приложением. При каждом запуске встроенные схемы (`author: md2gost`) синхронизируются с шаблоном: новые id добавляются, устаревшие (например удалённый `bpmn`) убираются, содержимое обновляется. Схемы с пустым или другим `author` не трогаются. Если правите встроенную в GUI и сохраняете — `author` сбрасывается, чтобы миграция не затёрла правки. | id | Название | Оградка | |----|----------|---------| @@ -46,9 +45,9 @@ Rel(user, app, "логин") | `c4context` | C4 Context | `` ```uml-c4context `` | | `c4component` | C4 Component | `` ```uml-c4component `` | | `usecase` | Прецеденты | `` ```uml-usecase `` / `` ```usecase `` | -| `bpmn` | BPMN 2.0 | `` ```bpmn `` / `` ```uml-bpmn `` | +| `archimate` | ArchiMate 3.2 | `` ```uml-archimate `` / `` ```archimate `` | -Для схем в теле пишите **только макросы** (Person, Start, XOR, …) — без `@startuml` и без `!include`, если схема сама их добавляет. +Для схем в теле пишите **только макросы** (Person, Rel, actor, Business_Actor, …) — без `@startuml` и без `!include`, если схема сама их добавляет. ### C4 (пример макросов) @@ -58,14 +57,151 @@ Rel(user, app, "логин") `actor`, `usecase`, связи `-->`, `<>`, `<>`. -### BPMN +### ArchiMate 3.2 -`Pool` / `Lane`, `Start` / `StartMessage` / `End`, `UserTask` / `ServiceTask`, `XOR` / `AND` / `OR`, `Flow` / `CondFlow` / `DefaultFlow` / `MessageFlow`, … - -Sequence Flow только внутри пула; между пулами — `MessageFlow`. +`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 @@ -114,7 +250,15 @@ id схемы: латиница, цифры, `_`; начинается с бук 3. QuickJS + mermaid.js (`mermaidx`) — офлайн без браузера 4. Локальный Kroki → `https://kroki.io` (если `--diagram-fallback remote`) -Формат: `--diagram-format png` (по умолчанию) или `svg`. Масштаб растра PlantUML / локального Mermaid: `--diagram-scale` (по умолчанию 2 — качество, размер на странице как при 1). +**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). @@ -124,3 +268,5 @@ id схемы: латиница, цифры, `_`; начинается с бук 1. Меню **Шаблоны UML** — добавить / править / сохранить. 2. Или править `md2gost.schemes.json` вручную («Открыть JSON»). 3. CLI: `--schemes путь.json`. + +Чтобы правка встроенной схемы не сбрасывалась при обновлении приложения, оставьте пустой `author` (GUI делает это сама при сохранении изменённой встроенной схемы). diff --git a/docs/title.md b/docs/title.md new file mode 100644 index 0000000..03f60a1 --- /dev/null +++ b/docs/title.md @@ -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` рядом с приложением (см. каталог схем). Не коммитьте его с чужими ФИО в общие репозитории отчётов — это локальные данные студента. diff --git a/docs/word2md.md b/docs/word2md.md new file mode 100644 index 0000000..b57871c --- /dev/null +++ b/docs/word2md.md @@ -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»**. + +Картинки пишутся в каталог `_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`. diff --git a/md2gost.fast.spec b/md2gost.fast.spec index e5e295e..b9020f3 100644 --- a/md2gost.fast.spec +++ b/md2gost.fast.spec @@ -1,11 +1,13 @@ # -*- mode: python ; coding: utf-8 -*- -"""Faster onefile freeze: same portable exe (fonts + plantuml.jar + onefile). +"""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 ( @@ -18,9 +20,14 @@ from PyInstaller.utils.hooks import ( 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"), @@ -33,9 +40,20 @@ if not _plantuml.is_file(): "сначала 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") @@ -95,10 +113,9 @@ hiddenimports += [ ] # Do NOT collect_submodules("pygments.lexers") here — hook-pygments.py already # collects all lexers/formatters/styles once during Analysis. -try: - hiddenimports += collect_submodules("tkinterdnd2") -except Exception: - pass +_dnd_datas, _dnd_hidden = collect_tkinterdnd2() +datas += _dnd_datas +hiddenimports += _dnd_hidden excludes = [ "pytest", @@ -150,10 +167,14 @@ a = Analysis( hooksconfig={ "matplotlib": {"backends": "Agg"}, }, - runtime_hooks=[str(ROOT / "scripts" / "pyi_rth_mplbackend.py")], + 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) diff --git a/md2gost.spec b/md2gost.spec index ba7e145..501d405 100644 --- a/md2gost.spec +++ b/md2gost.spec @@ -1,4 +1,5 @@ # -*- mode: python ; coding: utf-8 -*- +import sys from pathlib import Path from PyInstaller.utils.hooks import ( @@ -8,9 +9,14 @@ from PyInstaller.utils.hooks import ( ) 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"), @@ -19,8 +25,19 @@ datas = [ _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") @@ -78,10 +95,9 @@ hiddenimports += [ ] hiddenimports += collect_submodules("pygments.lexers") hiddenimports += collect_submodules("marko") -try: - hiddenimports += collect_submodules("tkinterdnd2") -except Exception: - pass +_dnd_datas, _dnd_hidden = collect_tkinterdnd2() +datas += _dnd_datas +hiddenimports += _dnd_hidden excludes = [ "pytest", @@ -111,10 +127,11 @@ a = Analysis( hiddenimports=hiddenimports, hookspath=[], hooksconfig={}, - runtime_hooks=[], + runtime_hooks=[str(ROOT / "scripts" / "pyi_rth_tcltk.py")], excludes=excludes, noarchive=False, ) +sanitize_analysis(a) pyz = PYZ(a.pure) diff --git a/md2gost.user.json b/md2gost.user.json new file mode 100644 index 0000000..0eea062 --- /dev/null +++ b/md2gost.user.json @@ -0,0 +1,14 @@ +{ + "student": "Тарасов Игорь Алексеевич", + "group": "ИНБО-31-23", + "metadata": { + "author_source": "student", + "author": "", + "last_modified_by": "", + "title": "", + "subject": "", + "keywords": "", + "comments": "Создано при помощи md2gost (ТЗ МИРЭА)", + "category": "" + } +} diff --git a/md2gost/README.md b/md2gost/README.md index 02d5cf7..df3b61a 100644 --- a/md2gost/README.md +++ b/md2gost/README.md @@ -102,7 +102,7 @@ python -m md2gost report.md -o report.docx --toc manual | Таблица | `%id Подпись` перед таблицей + `@Таблица:id` | | Склеивание ячеек | `^` — rowspan (ячейка сверху), `>` — colspan (ячейка слева) | | Листинг | `%id Подпись` перед code fence | -| Диаграмма UML / Mermaid / схемы | `%id Подпись` + ````uml` / ````uml-c4` / ````bpmn` / ````mermaid` → Рисунок; `+listing` — ещё и Листинг. Схемы в `md2gost.schemes.json`. IDEF0 нет | +| Диаграмма UML / Mermaid / IDEF0 / DFD / схемы | `%id Подпись` + ````uml` / ````uml-c4` / ````mermaid` / ````idef0` / ````dfd` → Рисунок; `+listing` — ещё и Листинг. Схемы в `md2gost.schemes.json` | | Формула | `%eq1` + `$$…$$` + `@Формула:eq1` (номер только при ссылке) | | Источник | `[1]` в тексте (кликабельная ссылка на пункт списка); `[1]: …` в списке | | Разрыв страницы | `---` на отдельной строке + `--hr-pagebreak` (по умолчанию `---` игнорируется) | @@ -136,7 +136,7 @@ Word COM режет по реальной пагинации. Без Word — у | Режим | Поведение | |--------|-----------| -| **`word`** (по умолчанию) | Как `off` при рендере; после save Word COM: `Split` + «Продолжение Таблицы N». Нужны Windows, Word, pywin32. Шапка на продолжении не повторяется (вкл: `--table-repeat-header`) | +| **`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` (могут быть дыры) | | **`caption`** | Режем по оценке + явный PageBreak + «Продолжение…» (то же ограничение точности) | @@ -208,34 +208,7 @@ Rel(user, app, "логин") ``` ```` -URL в `includes` схемы скачиваются в кэш (`md2gost.include-cache.json` + папка `include-cache/`). CLI: `--schemes path.json`. **BPMN 2.0** — оградка ````bpmn` / ````uml-bpmn` (макросы `Start`, `UserTask`, `XOR`, `Flow`, `Pool`…; библиотека `diagrams/BPMN.puml`). **Mermaid** — ````mermaid` / ````mmd`: системный браузер → Playwright Chromium → QuickJS (`mermaidx`) → Kroki (свой `--kroki-url` или kroki.io). **IDEF0** конвертер не рисует — вставляйте готовый PNG. - -```bpmn -StartMessage(s, "заявка") -UserTask(t, "Проверить") -XOR(gw, "ок?") -End(e_ok) -End(e_no) -Flow(s, t) -Flow(t, gw) -CondFlow(gw, e_ok, "да") -DefaultFlow(gw, e_no) -``` -````markdown -%bpmn1 Процесс заявки - -```bpmn -StartMessage(s, "заявка") -UserTask(t, "Проверить") -XOR(gw, "ок?") -End(e_ok) -End(e_no) -Flow(s, t) -Flow(t, gw) -CondFlow(gw, e_ok, "да") -DefaultFlow(gw, e_no) -``` -```` +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: @@ -250,6 +223,12 @@ Mermaid: 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-страница, затем снова книжная. @@ -260,6 +239,7 @@ Mermaid: ```bash 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 diff --git a/md2gost/TitleTemplate.docx b/md2gost/TitleTemplate.docx new file mode 100644 index 0000000000000000000000000000000000000000..f007e28d31af99bbea886c7338444cc8c4b749a0 GIT binary patch literal 206244 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 ArgumentParser: "Генерация DOCX-отчётов из Markdown по ТЗ МИРЭА / ГОСТ. " "Типы: coursework/practice/vkr, APID_coursework, PIS_custom. " "Без файла или с --gui открывается окно. " + "DOCX → Markdown: python -m word2md файл.docx (или md2gost файл.docx). " "FODT: python -m md2fodt …" ), ) parser.add_argument( "filename", nargs="?", - help="Путь до .md (или .docx с --check-pages). Без файла открывается GUI", + help="Путь до .md (сборка DOCX), .docx (импорт в Markdown) или .docx с --check-pages", ) parser.add_argument( "--gui", action="store_true", @@ -116,8 +118,75 @@ def build_parser() -> ArgumentParser: "По умолчанию такие строки игнорируются; включить: --hr-pagebreak." ), ) - parser.add_argument("--title", help="DOCX титульного листа (вставляется перед телом)") + 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("--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 по ТЗ и вывести отчёт", action="store_true") parser.add_argument("--check-only", help="Только проверка, без генерации документа", @@ -215,6 +284,17 @@ def request_from_args(args) -> ConvertRequest: 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)), diff --git a/md2gost/biblio_processor.py b/md2gost/biblio_processor.py index e93ed8d..9e4cc9a 100644 --- a/md2gost/biblio_processor.py +++ b/md2gost/biblio_processor.py @@ -39,6 +39,21 @@ def entries_from_text(text: str) -> list[BiblioEntry]: 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]]]: """ Parse bibliography from raw markdown (reliable even if Marko merges lines). @@ -126,8 +141,8 @@ def fold_bibliography(renderables: list[Renderable], parent, consumed += 1 continue if isinstance(item, Paragraph): - text = (item._docx_paragraph.text or "").strip() - if entries_from_text(text) or (not text): + text = item._docx_paragraph.text or "" + if is_biblio_source_paragraph(text): consumed += 1 continue break diff --git a/md2gost/converter.py b/md2gost/converter.py index b6a9a8d..2afcc90 100644 --- a/md2gost/converter.py +++ b/md2gost/converter.py @@ -46,7 +46,8 @@ class Converter: table_continuation: str = DEFAULT_TABLE_CONTINUATION, listing_continuation: str = DEFAULT_LISTING_CONTINUATION, hr_pagebreak: bool = False, - styles_path: str | None = None): + styles_path: str | None = None, + markdown_text: str | None = None): if heading_numbering not in HEADING_NUMBERING_MODES: raise ValueError( f"heading_numbering must be one of {HEADING_NUMBERING_MODES}, " @@ -79,6 +80,7 @@ class Converter: self._document: Document = docx.Document(template_path) self._document._body.clear_content() 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, @@ -86,8 +88,11 @@ class Converter: styles_path=styles_path, ) self._debugger = Debugger(self._document) if debug else None - with open(input_path, encoding="utf-8") as f: - raw = f.read() + if markdown_text is not None: + raw = markdown_text + else: + with open(input_path, encoding="utf-8") as f: + raw = f.read() self._raw_markdown = raw self._preprocessed = preprocess_markdown(raw, emdash_to_hyphen=emdash_to_hyphen) self.parser = Parser(self._document, self._preprocessed, hr_pagebreak=hr_pagebreak) diff --git a/md2gost/dfd.py b/md2gost/dfd.py new file mode 100644 index 0000000..78413d6 --- /dev/null +++ b/md2gost/dfd.py @@ -0,0 +1,477 @@ +"""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("", "", 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("", "", 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_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) + + ok_png = _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: + out_svg = Path(out_svg) + 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 diff --git a/md2gost/diagram_renderer.py b/md2gost/diagram_renderer.py index 1e46205..7e561b4 100644 --- a/md2gost/diagram_renderer.py +++ b/md2gost/diagram_renderer.py @@ -33,11 +33,16 @@ DIAGRAM_LANGS = frozenset( "c4context", "c4component", "usecase", - "bpmn", "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"] DiagramFormat = Literal["png", "svg"] @@ -255,6 +260,13 @@ def diagram_engine_status(explicit_jar: str | None = None) -> str: 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 @@ -424,14 +436,23 @@ def render_diagram( 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 - # Mermaid local hi-res uses a scale-specific cache key; Kroki uses unscaled key. + 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 == "mermaid" and scale > 1: + 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 == "mermaid" else cache_prefix, + 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 @@ -453,7 +474,7 @@ def render_diagram( 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 == "mermaid" and scale > 1 and local_prefix != cache_prefix: + 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), @@ -505,6 +526,30 @@ def render_diagram( 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: diff --git a/md2gost/diagram_schemes.py b/md2gost/diagram_schemes.py index f1f2c78..50064be 100644 --- a/md2gost/diagram_schemes.py +++ b/md2gost/diagram_schemes.py @@ -21,10 +21,33 @@ _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 @@ -82,22 +105,84 @@ 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: """ - On first run copy bundled template next to the app. - Never overwrite an existing user file. + 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 dest.is_file(): - return dest - 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) + 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 @@ -129,7 +214,7 @@ def load_schemes_from_path(path: Path) -> dict[str, DiagramScheme]: def save_schemes(schemes: dict[str, DiagramScheme], path: Path | None = None) -> Path: dest = path or user_schemes_path() - payload = {sid: scheme.to_dict() for sid, scheme in sorted(schemes.items())} + payload = _schemes_payload(schemes) dest.parent.mkdir(parents=True, exist_ok=True) dest.write_text( json.dumps(payload, ensure_ascii=False, indent=2) + "\n", @@ -138,12 +223,50 @@ def save_schemes(schemes: dict[str, DiagramScheme], path: Path | None = None) -> 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: + 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:] @@ -224,7 +347,7 @@ def get_scheme(scheme_id: str) -> DiagramScheme | None: 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: + 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. @@ -316,6 +439,10 @@ def prepare_with_schemes(lang: str, source: str, *, base: Path | None = None) -> 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: diff --git a/md2gost/diagrams/BPMN.puml b/md2gost/diagrams/BPMN.puml deleted file mode 100644 index 02b8c5b..0000000 --- a/md2gost/diagrams/BPMN.puml +++ /dev/null @@ -1,1126 +0,0 @@ -' md2gost BPMN 2.0 macros for PlantUML -' Notation: OMG BPMN 2.0.2 (formal/2013-12-09), Ch. 7 graphical elements, -' Ch. 10 Process (events, activities, gateways), Ch. 9 Collaboration (pools, message flow). -' PlantUML cannot attach boundary events to the activity border or draw a true -' double-circle; thickness + stereotypes approximate start / intermediate / end. -' Gateways use hexagon — in this renderer it reads as a BPMN diamond. -' -' Usage (scheme ```bpmn / ```uml-bpmn wraps @startuml + this include): -' Pool(company, "Компания") { -' Lane(op, "Оператор") { -' Start(s) -' UserTask(t, "Проверить") -' XOR(gw) -' End(e) -' Flow(s, t) -' Flow(t, gw) -' CondFlow(gw, e, "нет") -' } -' } - -!ifndef BPMN_PUML -!define BPMN_PUML - -hide stereotype -skinparam shadowing false -skinparam linetype polyline -skinparam nodesep 28 -skinparam ranksep 42 -skinparam defaultTextAlignment center -skinparam wrapWidth 140 -skinparam ArrowColor #212121 -skinparam Padding 2 - -' --------------------------------------------------------------------------- -' Sprites (event / task markers, BPMN 2.0 Table 7.2–7.4) -' --------------------------------------------------------------------------- - -sprite $bpmnMsg [16x11/16] { -0000000000000000 -0FFFFFFFFFFFFFF0 -0F000000000000F0 -0FF0000000000FF0 -0F0FF000000FF0F0 -0F000FFFFFF000F0 -0F0000FFFF0000F0 -0F000000000000F0 -0F000000000000F0 -0FFFFFFFFFFFFFF0 -0000000000000000 -} - -sprite $bpmnMsgFill [16x11/16] { -0000000000000000 -0FFFFFFFFFFFFFF0 -0FFFFFFFFFFFFFF0 -0FFFFFFFFFFFFFF0 -0FFFFFFFFFFFFFF0 -0FFF0FFFFFF0FFF0 -0FF000FFFF000FF0 -0F00000FF00000F0 -0F000000000000F0 -0FFFFFFFFFFFFFF0 -0000000000000000 -} - -sprite $bpmnTimer [16x10/16] { -000000FFFF000000 -0000FF0000FF0000 -000F000FF000F000 -00F0000FF0000F00 -00F0000FF0000F00 -00F0000FFFF00F00 -00F0000000000F00 -000F00000000F000 -0000FF0000FF0000 -000000FFFF000000 -} - -sprite $bpmnError [16x9/16] { -0000000FF0000000 -000000FF00000000 -00000FFFFF000000 -0000FF0000000000 -000FFFFF00000000 -000000FF00000000 -00000FF000000000 -0000FF0000000000 -000FF00000000000 -} - -sprite $bpmnSignal [16x8/16] { -00000000F0000000 -0000000FFF000000 -000000FF0FF00000 -00000FF000FF0000 -0000FF00000FF000 -000FF0000000FF00 -00FF000000000FF0 -0FFFFFFFFFFFFFFF -} - -sprite $bpmnSignalFill [16x8/16] { -00000000F0000000 -0000000FFF000000 -000000FFFFF00000 -00000FFFFFFF0000 -0000FFFFFFFFF000 -000FFFFFFFFFFF00 -00FFFFFFFFFFFFF0 -0FFFFFFFFFFFFFFF -} - -sprite $bpmnTerminate [16x9/16] { -00000FFFFFF00000 -000FFFFFFFFFF000 -00FFFFFFFFFFFF00 -0FFFFFFFFFFFFFF0 -0FFFFFFFFFFFFFF0 -0FFFFFFFFFFFFFF0 -00FFFFFFFFFFFF00 -000FFFFFFFFFF000 -00000FFFFFF00000 -} - -sprite $bpmnEscalation [16x9/16] { -00000000F0000000 -0000000FFF000000 -000000FF0FF00000 -00000FF000FF0000 -0000FFFFFFFFF000 -000000F000F00000 -000000F000F00000 -000000F000F00000 -000000FFFFF00000 -} - -sprite $bpmnComp [16x7/16] { -00F0000F00000000 -0FF000FF00000000 -FFF00FFF00000000 -FFFF0FFFF0000000 -FFF00FFF00000000 -0FF000FF00000000 -00F0000F00000000 -} - -sprite $bpmnLink [16x7/16] { -0000000000000000 -00000000000F0000 -00000000000FF000 -0FFFFFFFFFFFFF00 -00000000000FF000 -00000000000F0000 -0000000000000000 -} - -sprite $bpmnCond [16x10/16] { -0FFFFFFFFFF00000 -0F00000000FF0000 -0F00000000F0F000 -0F00000000FFF000 -0F00FFFF0000F000 -0F0000000000F000 -0F00FFFF0000F000 -0F0000000000F000 -0F00FFFF0000F000 -0FFFFFFFFFFFF000 -} - -sprite $bpmnMulti [16x8/16] { -00000000F0000000 -000000FF0FF00000 -00000F00000F0000 -0000F0000000F000 -000F000000000F00 -000F000000000F00 -0000F0000000F000 -00000FFFFFFF0000 -} - -sprite $bpmnParMulti [16x9/16] { -00000FFFFFF00000 -000FF000000FF000 -00F0000FF0000F00 -0F00000FF00000F0 -0F00FFFFFFFF00F0 -0F00000FF00000F0 -00F0000FF0000F00 -000FF000000FF000 -00000FFFFFF00000 -} - -sprite $bpmnCancel [16x11/16] { -0FF0000000000FF0 -00FF00000000FF00 -000FF000000FF000 -0000FF0000FF0000 -00000FF00FF00000 -000000FFFF000000 -00000FF00FF00000 -0000FF0000FF0000 -000FF000000FF000 -00FF00000000FF00 -0FF0000000000FF0 -} - -sprite $bpmnUser [16x10/16] { -000000FFFF000000 -00000FFFFFF00000 -00000FFFFFF00000 -000000FFFF000000 -0000000000000000 -0000FFFFFFFF0000 -000FF000000FF000 -000FF000000FF000 -000FF000000FF000 -000FF000000FF000 -} - -sprite $bpmnService [16x9/16] { -000000FFFF000000 -0000FFF00FFF0000 -000FF0FFFF0FF000 -00FF00FFFF00FF00 -00F0000000000F00 -00FF00FFFF00FF00 -000FF0FFFF0FF000 -0000FFF00FFF0000 -000000FFFF000000 -} - -sprite $bpmnScript [16x9/16] { -0FFFFFFFFFF00000 -0F00000000FF0000 -0F00000000F0F000 -0F00FFFF00FFF000 -0F0000000000F000 -0F00FFFF0000F000 -0F0000000000F000 -0F00FFFF0000F000 -0FFFFFFFFFFFF000 -} - -sprite $bpmnManual [16x7/16] { -000000000FF00000 -00000000FFFF0000 -0000FF0FF0FF0000 -000FFFFFFFFFF000 -000FFFFFFFFFF000 -0000FFFFFFFF0000 -00000FFFFFF00000 -} - -sprite $bpmnRule [16x7/16] { -0FFFFFFFFFFFFFF0 -0F000F000F0000F0 -0FFFFFFFFFFFFFF0 -0F000F000F0000F0 -0FFFFFFFFFFFFFF0 -0F000F000F0000F0 -0FFFFFFFFFFFFFF0 -} - -sprite $bpmnPlusBox [12x9/16] { -FFFFFFFFFFFF -F0000000000F -F0000FF0000F -F0000FF0000F -F0FFFFFFFF0F -F0000FF0000F -F0000FF0000F -F0000000000F -FFFFFFFFFFFF -} - -sprite $bpmnLoop [16x7/16] { -0000FFFFFF000000 -000F000000F00000 -00F000000000000F -0F00000000000FF0 -0F000000000F0000 -00F0000000F00000 -000FFFFFF0000000 -} - -sprite $bpmnAdhoc [16x5/16] { -0000000000000000 -0FF0000FFF000FF0 -000FF0F000FF0000 -00000F00000F0000 -0000000000000000 -} - -sprite $bpmnMIPar [13x5/16] { -00F000F000F00 -00F000F000F00 -00F000F000F00 -00F000F000F00 -00F000F000F00 -} - -sprite $bpmnMISeq [13x7/16] { -0000000000000 -0FFFFFFFFFFF0 -0000000000000 -0FFFFFFFFFFF0 -0000000000000 -0FFFFFFFFFFF0 -0000000000000 -} - -' --------------------------------------------------------------------------- -' Skin (colours are conventional, not mandated by the spec) -' --------------------------------------------------------------------------- - -skinparam circle<> { - BackgroundColor #C8E6C9 - BorderColor #2E7D32 - BorderThickness 1.5 - FontSize 10 -} - -skinparam circle<> { - BackgroundColor #C8E6C9 - BorderColor #2E7D32 - BorderThickness 1.5 - BorderStyle dashed - FontSize 10 -} - -skinparam circle<> { - BackgroundColor #FFF3E0 - BorderColor #EF6C00 - BorderThickness 2 - FontSize 10 -} - -skinparam circle<> { - BackgroundColor #FFE0B2 - BorderColor #E65100 - BorderThickness 2 - FontSize 10 -} - -skinparam circle<> { - BackgroundColor #FFCDD2 - BorderColor #B71C1C - BorderThickness 3.5 - FontSize 10 -} - -skinparam circle<> { - BackgroundColor #FFF3E0 - BorderColor #EF6C00 - BorderThickness 2 - FontSize 9 -} - -skinparam circle<> { - BackgroundColor #FFF3E0 - BorderColor #EF6C00 - BorderThickness 2 - BorderStyle dashed - FontSize 9 -} - -skinparam hexagon<> { - BackgroundColor #FFF8E1 - BorderColor #F9A825 - BorderThickness 1.5 - FontSize 12 -} - -skinparam rectangle<> { - RoundCorner 22 - BackgroundColor #FFFFFF - BorderColor #1565C0 - BorderThickness 1.4 - FontSize 11 -} - -skinparam rectangle<> { - RoundCorner 22 - BackgroundColor #FAFAFA - BorderColor #1565C0 - BorderThickness 1.4 - FontSize 11 -} - -skinparam rectangle<> { - RoundCorner 22 - BackgroundColor #FFFFFF - BorderColor #0D47A1 - BorderThickness 3 - FontSize 11 -} - -skinparam rectangle<> { - RoundCorner 22 - BackgroundColor #FAFAFA - BorderColor #0D47A1 - BorderThickness 2.5 - FontSize 11 -} - -skinparam rectangle<> { - RoundCorner 22 - BackgroundColor #FAFAFA - BorderColor #6A1B9A - BorderThickness 1.4 - BorderStyle dashed - FontSize 11 -} - -skinparam rectangle<> { - RoundCorner 0 - BackgroundColor #F5F5F5 - BorderColor #212121 - BorderThickness 1.6 - FontSize 12 - FontStyle bold -} - -skinparam rectangle<> { - RoundCorner 0 - BackgroundColor #FFFFFF - BorderColor #757575 - BorderThickness 1 - FontSize 11 -} - -skinparam rectangle<> { - RoundCorner 0 - BackgroundColor #EEEEEE - BorderColor #212121 - BorderThickness 1.6 - FontSize 12 - FontStyle bold -} - -skinparam rectangle<> { - RoundCorner 8 - BackgroundColor transparent - BorderColor #9E9E9E - BorderThickness 1.2 - BorderStyle dashed - FontSize 10 -} - -skinparam file<> { - BackgroundColor #E3F2FD - BorderColor #1565C0 - FontSize 10 -} - -skinparam database<> { - BackgroundColor #E8F5E9 - BorderColor #2E7D32 - FontSize 10 -} - -skinparam note { - BackgroundColor #FFFDE7 - BorderColor #F9A825 - FontSize 10 -} - -' --------------------------------------------------------------------------- -' Internals -' --------------------------------------------------------------------------- - -!unquoted procedure $bpmnCircle($alias, $stereo, $icon, $label) - !if ($icon == "") - !if ($label == "") - circle " " as $alias <<$stereo>> - !else - circle "$label" as $alias <<$stereo>> - !endif - !else - !if ($label == "") - circle "<$$icon>" as $alias <<$stereo>> - !else - circle "<$$icon>\n$label" as $alias <<$stereo>> - !endif - !endif -!endprocedure - -!unquoted procedure $bpmnGW($alias, $mark, $label) - !if ($label == "") - hexagon "$mark" as $alias <> - !else - hexagon "$mark\n$label" as $alias <> - !endif -!endprocedure - -!unquoted procedure $bpmnRect($alias, $stereo, $icon, $label, $marker="") - !if ($icon == "") - !if ($marker == "") - rectangle "$label" as $alias <<$stereo>> - !else - rectangle "$label\n\n<$$marker>" as $alias <<$stereo>> - !endif - !else - !if ($marker == "") - rectangle "<$$icon> $label" as $alias <<$stereo>> - !else - rectangle "<$$icon> $label\n\n<$$marker>" as $alias <<$stereo>> - !endif - !endif -!endprocedure - -' --------------------------------------------------------------------------- -' Swimlanes — BPMN 2.0 §9.2 Pool / §9.3 Lane -' Pool(...) { ... } Lane(...) { ... } -' --------------------------------------------------------------------------- - -!unquoted procedure Pool($alias, $label) - rectangle "$label" as $alias <> -!endprocedure - -!unquoted procedure Lane($alias, $label) - rectangle "$label" as $alias <> -!endprocedure - -!unquoted procedure BlackBoxPool($alias, $label) - rectangle "$label" as $alias <> -!endprocedure - -!unquoted procedure Participant($alias, $label) - BlackBoxPool($alias, $label) -!endprocedure - -!unquoted procedure Group($alias, $label) - rectangle "$label" as $alias <> -!endprocedure - -' --------------------------------------------------------------------------- -' Start events — §10.5.2 (None, Message, Timer, Conditional, Signal, Error, -' Escalation, Multiple, Parallel Multiple). Error/Escalation only in event sub-process. -' --------------------------------------------------------------------------- - -!unquoted procedure Start($alias, $label="") - $bpmnCircle($alias, "bpmnStart", "", $label) -!endprocedure - -!unquoted procedure StartEvent($alias, $label="") - Start($alias, $label) -!endprocedure - -!unquoted procedure StartMessage($alias, $label="") - $bpmnCircle($alias, "bpmnStart", "bpmnMsg", $label) -!endprocedure - -!unquoted procedure StartTimer($alias, $label="") - $bpmnCircle($alias, "bpmnStart", "bpmnTimer", $label) -!endprocedure - -!unquoted procedure StartCondition($alias, $label="") - $bpmnCircle($alias, "bpmnStart", "bpmnCond", $label) -!endprocedure - -!unquoted procedure StartConditional($alias, $label="") - StartCondition($alias, $label) -!endprocedure - -!unquoted procedure StartSignal($alias, $label="") - $bpmnCircle($alias, "bpmnStart", "bpmnSignal", $label) -!endprocedure - -!unquoted procedure StartError($alias, $label="") - $bpmnCircle($alias, "bpmnStart", "bpmnError", $label) -!endprocedure - -!unquoted procedure StartEscalation($alias, $label="") - $bpmnCircle($alias, "bpmnStart", "bpmnEscalation", $label) -!endprocedure - -!unquoted procedure StartMultiple($alias, $label="") - $bpmnCircle($alias, "bpmnStart", "bpmnMulti", $label) -!endprocedure - -!unquoted procedure StartParallelMultiple($alias, $label="") - $bpmnCircle($alias, "bpmnStart", "bpmnParMulti", $label) -!endprocedure - -!unquoted procedure StartNI($alias, $label="") - $bpmnCircle($alias, "bpmnStartNI", "", $label) -!endprocedure - -!unquoted procedure StartMessageNI($alias, $label="") - $bpmnCircle($alias, "bpmnStartNI", "bpmnMsg", $label) -!endprocedure - -!unquoted procedure StartTimerNI($alias, $label="") - $bpmnCircle($alias, "bpmnStartNI", "bpmnTimer", $label) -!endprocedure - -!unquoted procedure StartSignalNI($alias, $label="") - $bpmnCircle($alias, "bpmnStartNI", "bpmnSignal", $label) -!endprocedure - -' --------------------------------------------------------------------------- -' Intermediate catching / throwing — §10.5.4 -' --------------------------------------------------------------------------- - -!unquoted procedure Intermediate($alias, $label="") - $bpmnCircle($alias, "bpmnInter", "", $label) -!endprocedure - -!unquoted procedure CatchMessage($alias, $label="") - $bpmnCircle($alias, "bpmnInter", "bpmnMsg", $label) -!endprocedure - -!unquoted procedure ThrowMessage($alias, $label="") - $bpmnCircle($alias, "bpmnThrow", "bpmnMsgFill", $label) -!endprocedure - -!unquoted procedure CatchTimer($alias, $label="") - $bpmnCircle($alias, "bpmnInter", "bpmnTimer", $label) -!endprocedure - -!unquoted procedure CatchSignal($alias, $label="") - $bpmnCircle($alias, "bpmnInter", "bpmnSignal", $label) -!endprocedure - -!unquoted procedure ThrowSignal($alias, $label="") - $bpmnCircle($alias, "bpmnThrow", "bpmnSignalFill", $label) -!endprocedure - -!unquoted procedure CatchError($alias, $label="") - $bpmnCircle($alias, "bpmnInter", "bpmnError", $label) -!endprocedure - -!unquoted procedure CatchEscalation($alias, $label="") - $bpmnCircle($alias, "bpmnInter", "bpmnEscalation", $label) -!endprocedure - -!unquoted procedure ThrowEscalation($alias, $label="") - $bpmnCircle($alias, "bpmnThrow", "bpmnEscalation", $label) -!endprocedure - -!unquoted procedure CatchCondition($alias, $label="") - $bpmnCircle($alias, "bpmnInter", "bpmnCond", $label) -!endprocedure - -!unquoted procedure CatchLink($alias, $label="") - $bpmnCircle($alias, "bpmnInter", "bpmnLink", $label) -!endprocedure - -!unquoted procedure ThrowLink($alias, $label="") - $bpmnCircle($alias, "bpmnThrow", "bpmnLink", $label) -!endprocedure - -!unquoted procedure CatchCompensation($alias, $label="") - $bpmnCircle($alias, "bpmnInter", "bpmnComp", $label) -!endprocedure - -!unquoted procedure ThrowCompensation($alias, $label="") - $bpmnCircle($alias, "bpmnThrow", "bpmnComp", $label) -!endprocedure - -!unquoted procedure CatchCancel($alias, $label="") - $bpmnCircle($alias, "bpmnInter", "bpmnCancel", $label) -!endprocedure - -!unquoted procedure CatchMultiple($alias, $label="") - $bpmnCircle($alias, "bpmnInter", "bpmnMulti", $label) -!endprocedure - -!unquoted procedure ThrowMultiple($alias, $label="") - $bpmnCircle($alias, "bpmnThrow", "bpmnMulti", $label) -!endprocedure - -!unquoted procedure CatchParallelMultiple($alias, $label="") - $bpmnCircle($alias, "bpmnInter", "bpmnParMulti", $label) -!endprocedure - -' --------------------------------------------------------------------------- -' Boundary events — interrupting (solid) / non-interrupting (dashed). Attach() -' --------------------------------------------------------------------------- - -!unquoted procedure BoundaryMessage($alias, $label="") - $bpmnCircle($alias, "bpmnBoundary", "bpmnMsg", $label) -!endprocedure - -!unquoted procedure BoundaryTimer($alias, $label="") - $bpmnCircle($alias, "bpmnBoundary", "bpmnTimer", $label) -!endprocedure - -!unquoted procedure BoundaryError($alias, $label="") - $bpmnCircle($alias, "bpmnBoundary", "bpmnError", $label) -!endprocedure - -!unquoted procedure BoundaryEscalation($alias, $label="") - $bpmnCircle($alias, "bpmnBoundary", "bpmnEscalation", $label) -!endprocedure - -!unquoted procedure BoundarySignal($alias, $label="") - $bpmnCircle($alias, "bpmnBoundary", "bpmnSignal", $label) -!endprocedure - -!unquoted procedure BoundaryCancel($alias, $label="") - $bpmnCircle($alias, "bpmnBoundary", "bpmnCancel", $label) -!endprocedure - -!unquoted procedure BoundaryCompensation($alias, $label="") - $bpmnCircle($alias, "bpmnBoundary", "bpmnComp", $label) -!endprocedure - -!unquoted procedure BoundaryCondition($alias, $label="") - $bpmnCircle($alias, "bpmnBoundary", "bpmnCond", $label) -!endprocedure - -!unquoted procedure BoundaryMessageNI($alias, $label="") - $bpmnCircle($alias, "bpmnBoundaryNI", "bpmnMsg", $label) -!endprocedure - -!unquoted procedure BoundaryTimerNI($alias, $label="") - $bpmnCircle($alias, "bpmnBoundaryNI", "bpmnTimer", $label) -!endprocedure - -!unquoted procedure BoundaryEscalationNI($alias, $label="") - $bpmnCircle($alias, "bpmnBoundaryNI", "bpmnEscalation", $label) -!endprocedure - -!unquoted procedure BoundarySignalNI($alias, $label="") - $bpmnCircle($alias, "bpmnBoundaryNI", "bpmnSignal", $label) -!endprocedure - -!unquoted procedure BoundaryConditionNI($alias, $label="") - $bpmnCircle($alias, "bpmnBoundaryNI", "bpmnCond", $label) -!endprocedure - -!unquoted procedure Attach($activity, $event) - $activity -[hidden]down- $event -!endprocedure - -!unquoted procedure Attach_R($activity, $event) - $activity -[hidden]right- $event -!endprocedure - -' --------------------------------------------------------------------------- -' End events — §10.5.3 (None, Message, Error, Escalation, Cancel, Compensation, -' Signal, Multiple, Terminate) -' --------------------------------------------------------------------------- - -!unquoted procedure End($alias, $label="") - $bpmnCircle($alias, "bpmnEnd", "", $label) -!endprocedure - -!unquoted procedure EndEvent($alias, $label="") - End($alias, $label) -!endprocedure - -!unquoted procedure EndMessage($alias, $label="") - $bpmnCircle($alias, "bpmnEnd", "bpmnMsgFill", $label) -!endprocedure - -!unquoted procedure EndError($alias, $label="") - $bpmnCircle($alias, "bpmnEnd", "bpmnError", $label) -!endprocedure - -!unquoted procedure EndEscalation($alias, $label="") - $bpmnCircle($alias, "bpmnEnd", "bpmnEscalation", $label) -!endprocedure - -!unquoted procedure EndCancel($alias, $label="") - $bpmnCircle($alias, "bpmnEnd", "bpmnCancel", $label) -!endprocedure - -!unquoted procedure EndCompensation($alias, $label="") - $bpmnCircle($alias, "bpmnEnd", "bpmnComp", $label) -!endprocedure - -!unquoted procedure EndSignal($alias, $label="") - $bpmnCircle($alias, "bpmnEnd", "bpmnSignalFill", $label) -!endprocedure - -!unquoted procedure EndMultiple($alias, $label="") - $bpmnCircle($alias, "bpmnEnd", "bpmnMulti", $label) -!endprocedure - -!unquoted procedure EndTerminate($alias, $label="") - $bpmnCircle($alias, "bpmnEnd", "bpmnTerminate", $label) -!endprocedure - -!unquoted procedure Terminate($alias, $label="") - EndTerminate($alias, $label) -!endprocedure - -' --------------------------------------------------------------------------- -' Activities — §10.3 Task types + markers (loop, MI, compensation, ad-hoc) -' --------------------------------------------------------------------------- - -!unquoted procedure Task($alias, $label, $type="none", $marker="") - !if ($type == "user") - $bpmnRect($alias, "bpmnTask", "bpmnUser", $label, $marker) - !elseif ($type == "service") - $bpmnRect($alias, "bpmnTask", "bpmnService", $label, $marker) - !elseif ($type == "script") - $bpmnRect($alias, "bpmnTask", "bpmnScript", $label, $marker) - !elseif ($type == "manual") - $bpmnRect($alias, "bpmnTask", "bpmnManual", $label, $marker) - !elseif ($type == "send") - $bpmnRect($alias, "bpmnTask", "bpmnMsgFill", $label, $marker) - !elseif ($type == "receive") - $bpmnRect($alias, "bpmnTask", "bpmnMsg", $label, $marker) - !elseif ($type == "rule") - $bpmnRect($alias, "bpmnTask", "bpmnRule", $label, $marker) - !elseif ($type == "businessrule") - $bpmnRect($alias, "bpmnTask", "bpmnRule", $label, $marker) - !else - $bpmnRect($alias, "bpmnTask", "", $label, $marker) - !endif -!endprocedure - -!unquoted procedure UserTask($alias, $label, $marker="") - Task($alias, $label, "user", $marker) -!endprocedure - -!unquoted procedure ServiceTask($alias, $label, $marker="") - Task($alias, $label, "service", $marker) -!endprocedure - -!unquoted procedure ScriptTask($alias, $label, $marker="") - Task($alias, $label, "script", $marker) -!endprocedure - -!unquoted procedure ManualTask($alias, $label, $marker="") - Task($alias, $label, "manual", $marker) -!endprocedure - -!unquoted procedure SendTask($alias, $label, $marker="") - Task($alias, $label, "send", $marker) -!endprocedure - -!unquoted procedure ReceiveTask($alias, $label, $marker="") - Task($alias, $label, "receive", $marker) -!endprocedure - -!unquoted procedure BusinessRuleTask($alias, $label, $marker="") - Task($alias, $label, "rule", $marker) -!endprocedure - -!unquoted procedure LoopTask($alias, $label) - Task($alias, $label, "none", "bpmnLoop") -!endprocedure - -!unquoted procedure SubProcess($alias, $label) - $bpmnRect($alias, "bpmnSubProcess", "", $label, "bpmnPlusBox") -!endprocedure - -!unquoted procedure SubProcessExpanded($alias, $label) - rectangle "$label" as $alias <> -!endprocedure - -!unquoted procedure CallActivity($alias, $label) - $bpmnRect($alias, "bpmnCall", "", $label, "bpmnPlusBox") -!endprocedure - -!unquoted procedure Transaction($alias, $label) - $bpmnRect($alias, "bpmnTransaction", "", $label, "bpmnPlusBox") -!endprocedure - -!unquoted procedure EventSubProcess($alias, $label) - rectangle "$label" as $alias <> -!endprocedure - -!unquoted procedure AdHocSubProcess($alias, $label) - $bpmnRect($alias, "bpmnSubProcess", "", $label, "bpmnAdhoc") -!endprocedure - -' --------------------------------------------------------------------------- -' Gateways — §10.6 (Exclusive, Parallel, Inclusive, Complex, Event-Based) -' --------------------------------------------------------------------------- - -!unquoted procedure Exclusive($alias, $label="") - $bpmnGW($alias, "×", $label) -!endprocedure - -!unquoted procedure XOR($alias, $label="") - Exclusive($alias, $label) -!endprocedure - -!unquoted procedure Xor($alias, $label="") - Exclusive($alias, $label) -!endprocedure - -!unquoted procedure Gateway($alias, $label="") - Exclusive($alias, $label) -!endprocedure - -!unquoted procedure Parallel($alias, $label="") - $bpmnGW($alias, "+", $label) -!endprocedure - -!unquoted procedure AND($alias, $label="") - Parallel($alias, $label) -!endprocedure - -!unquoted procedure And($alias, $label="") - Parallel($alias, $label) -!endprocedure - -!unquoted procedure Inclusive($alias, $label="") - $bpmnGW($alias, "○", $label) -!endprocedure - -!unquoted procedure OR($alias, $label="") - Inclusive($alias, $label) -!endprocedure - -!unquoted procedure Or($alias, $label="") - Inclusive($alias, $label) -!endprocedure - -!unquoted procedure ComplexGW($alias, $label="") - $bpmnGW($alias, "*", $label) -!endprocedure - -!unquoted procedure Complex($alias, $label="") - ComplexGW($alias, $label) -!endprocedure - -!unquoted procedure EventBased($alias, $label="") - $bpmnGW($alias, "◇", $label) -!endprocedure - -!unquoted procedure EventBasedGateway($alias, $label="") - EventBased($alias, $label) -!endprocedure - -!unquoted procedure ExclusiveEventBased($alias, $label="") - $bpmnGW($alias, "◇", $label) -!endprocedure - -!unquoted procedure ParallelEventBased($alias, $label="") - $bpmnGW($alias, "⊕", $label) -!endprocedure - -' --------------------------------------------------------------------------- -' Connecting objects — §8.4.13 Sequence Flow, Message Flow, Association -' Default = slash; conditional = mini-diamond (label prefixes as stand-in) -' --------------------------------------------------------------------------- - -!unquoted procedure Flow($from, $to, $label="") - !if ($label == "") - $from --> $to - !else - $from --> $to : $label - !endif -!endprocedure - -!unquoted procedure Flow_R($from, $to, $label="") - !if ($label == "") - $from -right-> $to - !else - $from -right-> $to : $label - !endif -!endprocedure - -!unquoted procedure Flow_L($from, $to, $label="") - !if ($label == "") - $from -left-> $to - !else - $from -left-> $to : $label - !endif -!endprocedure - -!unquoted procedure Flow_D($from, $to, $label="") - !if ($label == "") - $from -down-> $to - !else - $from -down-> $to : $label - !endif -!endprocedure - -!unquoted procedure Flow_U($from, $to, $label="") - !if ($label == "") - $from -up-> $to - !else - $from -up-> $to : $label - !endif -!endprocedure - -!unquoted procedure SequenceFlow($from, $to, $label="") - Flow($from, $to, $label) -!endprocedure - -!unquoted procedure CondFlow($from, $to, $cond) - $from --> $to : ◇ $cond -!endprocedure - -!unquoted procedure DefaultFlow($from, $to, $label="") - !if ($label == "") - $from --> $to : ╱ - !else - $from --> $to : ╱ $label - !endif -!endprocedure - -!unquoted procedure MessageFlow($from, $to, $label="") - !if ($label == "") - $from ..> $to - !else - $from ..> $to : $label - !endif -!endprocedure - -!unquoted procedure MessageFlow_R($from, $to, $label="") - !if ($label == "") - $from -[dashed]right-> $to - !else - $from -[dashed]right-> $to : $label - !endif -!endprocedure - -!unquoted procedure Assoc($from, $to, $label="") - !if ($label == "") - $from .. $to - !else - $from .. $to : $label - !endif -!endprocedure - -!unquoted procedure Association($from, $to, $label="") - Assoc($from, $to, $label) -!endprocedure - -!unquoted procedure DataAssoc($from, $to, $label="") - !if ($label == "") - $from ..> $to - !else - $from ..> $to : $label - !endif -!endprocedure - -' --------------------------------------------------------------------------- -' Data / artifacts — §7.1.1.7 Data, Group, Text Annotation -' --------------------------------------------------------------------------- - -!unquoted procedure DataObject($alias, $label) - file "$label" as $alias <> -!endprocedure - -!unquoted procedure DataInput($alias, $label) - file "▷ $label" as $alias <> -!endprocedure - -!unquoted procedure DataOutput($alias, $label) - file "$label ▷" as $alias <> -!endprocedure - -!unquoted procedure Collection($alias, $label) - file "$label\n<$bpmnMIPar>" as $alias <> -!endprocedure - -!unquoted procedure DataStore($alias, $label) - database "$label" as $alias <> -!endprocedure - -!unquoted procedure Annotation($alias, $text) - note "$text" as $alias -!endprocedure - -!unquoted procedure TextAnnotation($alias, $text) - Annotation($alias, $text) -!endprocedure - -' --------------------------------------------------------------------------- -' Layout helpers -' --------------------------------------------------------------------------- - -!unquoted procedure Lay_R($from, $to) - $from -[hidden]right-> $to -!endprocedure - -!unquoted procedure Lay_L($from, $to) - $from -[hidden]left-> $to -!endprocedure - -!unquoted procedure Lay_D($from, $to) - $from -[hidden]down-> $to -!endprocedure - -!unquoted procedure Lay_U($from, $to) - $from -[hidden]up-> $to -!endprocedure - -!procedure BPMN_HORIZONTAL() - left to right direction -!endprocedure - -!procedure BPMN_VERTICAL() - top to bottom direction -!endprocedure - -!procedure BPMN_ORTHO() - skinparam linetype ortho -!endprocedure - -!procedure BPMN_POLYLINE() - skinparam linetype polyline -!endprocedure - -!procedure BPMN_LEGEND() -legend right - BPMN 2.0 - |= Элемент |= Смысл | - | тонкий круг | Start Event | - | двойная толщина | Intermediate | - | жирный круг | End Event | - | ● внутри конца | Terminate | - | скруглённый прямоугольник | Activity / Task | - | ромб × | Exclusive (XOR) | - | ромб + | Parallel (AND) | - | ромб ○ | Inclusive (OR) | - | ромб ◇ | Event-Based | - | сплошная стрелка | Sequence Flow | - | пунктир | Message Flow | - | ╱ | Default Flow | - | ◇ на стрелке | Conditional Flow | -endlegend -!endprocedure - -!endif diff --git a/md2gost/diagrams/schemes.json b/md2gost/diagrams/schemes.json index e786982..bd3578e 100644 --- a/md2gost/diagrams/schemes.json +++ b/md2gost/diagrams/schemes.json @@ -45,16 +45,14 @@ "prefix": "@startuml\nleft to right direction\nskinparam actorStyle awesome\n", "postfix": "\n@enduml" }, - "bpmn": { - "title": "BPMN 2.0", + "archimate": { + "title": "ArchiMate 3.2", "version": "1.0.0", "author": "md2gost", - "docs": "Pool(alias, \"Участник\") { Lane(alias, \"Роль\") { … } }\nStart / StartMessage / StartTimer / StartSignal\nCatchMessage / ThrowMessage / CatchTimer / CatchError\nEnd / EndMessage / EndError / EndTerminate\nTask(alias, \"Имя\") / UserTask / ServiceTask / ScriptTask / ManualTask / SendTask / ReceiveTask / BusinessRuleTask\nSubProcess / SubProcessExpanded { … } / CallActivity\nXOR / AND / OR / EventBased / ComplexGW\nFlow / Flow_R/L/U/D / CondFlow(from, to, \"условие\") / DefaultFlow / MessageFlow\nDataObject / DataStore / Annotation\nBoundaryError + Attach(task, event)\nBPMN_VERTICAL() / BPMN_LEGEND()", - "ai-prompt": "Рисуй BPMN 2.0 (OMG). В блоке ```uml-bpmn (или ```bpmn) только макросы библиотеки md2gost. Без @startuml и без !include.\n\nПравила: Sequence Flow (Flow) только внутри одного пула. Между пулами — MessageFlow. У процесса есть Start и хотя бы один End. Развилка и слияние — шлюзы (XOR/AND/OR), не рисуй ветвление со стрелок задачи без шлюза, если это решение. Подпись условия — на CondFlow/Flow от шлюза. DefaultFlow — ветка «иначе». Не используй activity-syntax (start/:task/;), mxgraph и чистый UML.\n\nМакросы:\nПул: Pool(alias, \"Имя\") { Lane(alias, \"Роль\") { ... } }\nСобытия: Start, StartMessage, StartTimer, StartSignal, StartCondition, Intermediate, CatchMessage, ThrowMessage, CatchTimer, CatchSignal, ThrowSignal, CatchError, End, EndMessage, EndError, EndTerminate, EndSignal\nЗадачи: Task, UserTask, ServiceTask, ScriptTask, ManualTask, SendTask, ReceiveTask, BusinessRuleTask, SubProcess, SubProcessExpanded { ... }, CallActivity\nШлюзы: Exclusive/XOR, Parallel/AND, Inclusive/OR, EventBased, ComplexGW\nПотоки: Flow, Flow_R, Flow_L, Flow_U, Flow_D, CondFlow(from,to,\"условие\"), DefaultFlow, MessageFlow, Assoc, DataAssoc\nДанные: DataObject, DataStore, Annotation\nГраница: BoundaryError/Timer/Message + Attach(task, event)\nМакет: Lay_R/L/U/D, BPMN_VERTICAL(), BPMN_LEGEND()\nПсевдонимы элементов — латиница (s, t1, gw, e_ok). Не называй alias зарезервированными словами end/start/group.", - "includes": [ - "BPMN.puml" - ], - "prefix": "@startuml\nleft to right direction\n", + "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 \n!theme archimate-standard from \n", "postfix": "\n@enduml" } } diff --git a/md2gost/dnd.py b/md2gost/dnd.py index 7ce61cb..dade595 100644 --- a/md2gost/dnd.py +++ b/md2gost/dnd.py @@ -65,6 +65,13 @@ def first_markdown(paths: list[str]) -> str | None: 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: diff --git a/md2gost/doc_metadata.py b/md2gost/doc_metadata.py new file mode 100644 index 0000000..d2500fa --- /dev/null +++ b/md2gost/doc_metadata.py @@ -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 diff --git a/md2gost/gui.py b/md2gost/gui.py index b8f0d05..4617fd7 100644 --- a/md2gost/gui.py +++ b/md2gost/gui.py @@ -13,6 +13,7 @@ from .diagram_renderer import diagram_engine_status from .diagram_schemes import ( DiagramScheme, SCHEME_ID_RE, + claim_user_scheme_if_diverged, ensure_user_schemes, get_schemes, load_schemes_from_path, @@ -20,7 +21,7 @@ from .diagram_schemes import ( save_schemes, user_schemes_path, ) -from .dnd import enable_file_drop, first_markdown +from .dnd import enable_file_drop, first_docx, first_markdown from .help_content import ( SCHEMES_HELP, USAGE_HELP, @@ -200,6 +201,8 @@ class Md2GostApp: self._win_files: tk.Toplevel | None = None self._win_diagrams: tk.Toplevel | None = None self._win_schemes: tk.Toplevel | None = None + self._win_student: tk.Toplevel | None = None + self._win_meta: tk.Toplevel | None = None self._win_help: dict[str, tk.Toplevel] = {} ensure_user_schemes() @@ -209,6 +212,7 @@ class Md2GostApp: self._build() self._apply_request(self.req) self.root.after(300, self._setup_dnd) + self.root.after(400, self._ensure_student_profile) if self.req.filename: self._set_markdown(self.req.filename, from_user_output=bool(self.req.output)) @@ -229,6 +233,7 @@ class Md2GostApp: style.configure("Title.TLabel", font=font_title, foreground=NAVY, background=PAPER) style.configure("Hint.TLabel", foreground=MUTED, background=PAPER) style.configure("TCheckbutton", background=PAPER, font=font) + style.configure("TRadiobutton", background=PAPER, foreground=INK, font=font) style.configure("TButton", font=font, padding=(10, 4)) style.configure("TEntry", padding=3) style.configure("TCombobox", padding=3) @@ -242,6 +247,7 @@ class Md2GostApp: self.listing_var = tk.StringVar() self.emdash_var = tk.BooleanVar(value=False) self.hr_pagebreak_var = tk.BooleanVar(value=False) + self.auto_title_var = tk.BooleanVar(value=False) self.open_var = tk.BooleanVar(value=True) self.check_var = tk.BooleanVar(value=False) self.check_only_var = tk.BooleanVar(value=False) @@ -268,6 +274,8 @@ class Md2GostApp: settings = tk.Menu(menubar, tearoff=0) settings.add_command(label="Файлы…", command=self._open_files_window) + settings.add_command(label="Студент…", command=self._open_student_window) + settings.add_command(label="Метаданные…", command=self._open_metadata_window) settings.add_command(label="Диаграммы…", command=self._open_diagrams_window) menubar.add_cascade(label="Настройки", menu=settings) @@ -340,6 +348,8 @@ class Md2GostApp: hint = ttk.Label( frame, text="Пустой шаблон — встроенный Template.docx. Титул и задание вставляются перед телом отчёта.\n" + "Автотитул (info_conv.yaml / ```title) — галочка «Автотитул» на главном окне " + "(по умолчанию выкл.; ФИО/группа — Настройки → Студент…).\n" "Стили JSON — опционально; также подхватывается md2gost.styles.json рядом с .md.", style="Hint.TLabel", wraplength=560, @@ -348,6 +358,225 @@ class Md2GostApp: win.protocol("WM_DELETE_WINDOW", lambda: self._close_toplevel(win, "_win_files")) return win + def _ensure_student_profile(self) -> None: + from .user_profile import load_user_profile + + if load_user_profile().is_complete(): + return + self._open_student_window(first_run=True) + + def _open_student_window(self, first_run: bool = False) -> None: + existing = getattr(self, "_win_student", None) + if existing is not None and existing.winfo_exists(): + existing.lift() + existing.focus_force() + return + self._win_student = self._create_student_window(first_run=first_run) + + def _create_student_window(self, *, first_run: bool = False) -> tk.Toplevel: + from .user_profile import load_user_profile, save_user_profile + + profile = load_user_profile() + win = tk.Toplevel(self.root) + win.title("Данные студента" if first_run else "Настройки — Студент") + win.configure(bg=PAPER) + win.minsize(420, 180) + win.geometry("480x200") + win.transient(self.root) + if first_run: + win.grab_set() + frame = ttk.Frame(win, padding=12) + frame.pack(fill=tk.BOTH, expand=True) + frame.columnconfigure(1, weight=1) + + ttk.Label( + frame, + text="ФИО и группа нужны для автотитульника (info_conv.yaml / ```title).", + style="Hint.TLabel", + wraplength=440, + ).grid(row=0, column=0, columnspan=2, sticky="w", pady=(0, 8)) + + student_var = tk.StringVar(value=profile.student) + group_var = tk.StringVar(value=profile.group) + ttk.Label(frame, text="ФИО").grid(row=1, column=0, sticky="w", pady=4) + ttk.Entry(frame, textvariable=student_var).grid(row=1, column=1, sticky="ew", pady=4, padx=(8, 0)) + ttk.Label(frame, text="Группа").grid(row=2, column=0, sticky="w", pady=4) + ttk.Entry(frame, textvariable=group_var).grid(row=2, column=1, sticky="ew", pady=4, padx=(8, 0)) + + def _save(close: bool = True) -> None: + student = student_var.get().strip() + group = group_var.get().strip() + if not student or not group: + messagebox.showwarning( + "Данные студента", + "Заполните ФИО и группу.", + parent=win, + ) + return + current = load_user_profile() + current.student = student + current.group = group + save_user_profile(current) + if close: + self._close_toplevel(win, "_win_student") + + btns = ttk.Frame(frame) + btns.grid(row=3, column=0, columnspan=2, sticky="e", pady=(12, 0)) + ttk.Button(btns, text="Сохранить", command=lambda: _save(True)).pack(side=tk.RIGHT) + + def _on_close() -> None: + if first_run and not load_user_profile().is_complete(): + # Allow closing only after save; remind once + if not student_var.get().strip() or not group_var.get().strip(): + messagebox.showinfo( + "Данные студента", + "Можно заполнить позже: Настройки → Студент…\n" + "Без ФИО/группы автотитул не соберётся.", + parent=win, + ) + self._close_toplevel(win, "_win_student") + + win.protocol("WM_DELETE_WINDOW", _on_close) + return win + + def _open_metadata_window(self) -> None: + self._win_meta = self._raise_or_create(self._win_meta, self._create_metadata_window) + + def _create_metadata_window(self) -> tk.Toplevel: + from .doc_metadata import ( + AUTHOR_SOURCES, + DEFAULT_AUTHOR_SOURCE, + DEFAULT_DOC_COMMENTS, + DocumentMetadata, + os_user, + ) + from .user_profile import load_user_profile, save_user_profile + + profile = load_user_profile() + meta = profile.metadata or DocumentMetadata() + win = tk.Toplevel(self.root) + win.title("Настройки — Метаданные") + win.configure(bg=PAPER) + win.minsize(520, 420) + win.geometry("580x500") + win.transient(self.root) + frame = ttk.Frame(win, padding=12) + frame.pack(fill=tk.BOTH, expand=True) + frame.columnconfigure(1, weight=1) + + ttk.Label( + frame, + text=( + "Свойства файла Word (Файл → сведения): автор, название, тема, теги, примечание. " + "Хранятся в md2gost.user.json рядом с приложением." + ), + style="Hint.TLabel", + wraplength=540, + ).grid(row=0, column=0, columnspan=2, sticky="w", pady=(0, 8)) + + src_value = meta.author_source if meta.author_source in AUTHOR_SOURCES else DEFAULT_AUTHOR_SOURCE + author_src = tk.StringVar(value=src_value) + author_var = tk.StringVar(value=meta.author) + last_mod_var = tk.StringVar(value=meta.last_modified_by) + title_var = tk.StringVar(value=meta.title) + subject_var = tk.StringVar(value=meta.subject) + keywords_var = tk.StringVar(value=meta.keywords) + category_var = tk.StringVar(value=meta.category) + comments_var = tk.StringVar(value=meta.comments) + + ttk.Label(frame, text="Автор").grid(row=1, column=0, sticky="nw", pady=(4, 0)) + src_box = ttk.Frame(frame) + src_box.grid(row=1, column=1, sticky="ew", pady=(4, 0), padx=(8, 0)) + os_name = os_user() or "—" + student_name = (profile.student or "").strip() + student_text = ( + f"ФИО студента ({student_name})" + if student_name + else "ФИО студента (сначала Настройки → Студент…)" + ) + ttk.Radiobutton( + src_box, text=f"Имя пользователя Windows ({os_name})", + variable=author_src, value="os", + ).pack(anchor="w") + ttk.Radiobutton( + src_box, text=student_text, variable=author_src, value="student", + ).pack(anchor="w") + ttk.Radiobutton( + src_box, text="Указать вручную", variable=author_src, value="custom", + ).pack(anchor="w") + + author_entry = ttk.Entry(frame, textvariable=author_var) + author_entry.grid(row=2, column=1, sticky="ew", pady=(2, 4), padx=(8, 0)) + _tip(author_entry, "Используется, если выбран пункт «Указать вручную»") + + def _sync_author_entry(*_a) -> None: + state = "normal" if author_src.get() == "custom" else "disabled" + try: + author_entry.configure(state=state) + except tk.TclError: + pass + + author_src.trace_add("write", _sync_author_entry) + _sync_author_entry() + + def _row(row: int, label: str, var: tk.StringVar, tip: str) -> None: + ttk.Label(frame, text=label).grid(row=row, column=0, sticky="w", pady=4) + entry = ttk.Entry(frame, textvariable=var) + entry.grid(row=row, column=1, sticky="ew", pady=4, padx=(8, 0)) + _tip(entry, tip) + + _row(3, "Кем изменён", last_mod_var, "Пусто — то же значение, что у автора") + _row(4, "Название", title_var, "dc:title в свойствах файла; пусто — не задавать") + _row(5, "Тема", subject_var, "Тема документа в свойствах файла") + _row(6, "Ключевые слова", keywords_var, "Теги в свойствах файла Word") + _row(7, "Категория", category_var, "Категория в свойствах файла") + _row( + 8, + "Примечание", + comments_var, + f"По умолчанию: «{DEFAULT_DOC_COMMENTS}». Пусто — не писать примечание.", + ) + ttk.Label( + frame, + text="Пустое примечание убирает и текст md2gost, и «generated by python-docx» из шаблона.", + style="Hint.TLabel", + wraplength=540, + ).grid(row=9, column=0, columnspan=2, sticky="w", pady=(4, 0)) + + def _save() -> None: + src = author_src.get() if author_src.get() in AUTHOR_SOURCES else DEFAULT_AUTHOR_SOURCE + current = load_user_profile() + current.metadata = DocumentMetadata( + author_source=src, + author=author_var.get().strip(), + last_modified_by=last_mod_var.get().strip(), + title=title_var.get().strip(), + subject=subject_var.get().strip(), + keywords=keywords_var.get().strip(), + comments=comments_var.get().strip(), + category=category_var.get().strip(), + ) + save_user_profile(current) + self._close_toplevel(win, "_win_meta") + + def _reset() -> None: + author_src.set(DEFAULT_AUTHOR_SOURCE) + author_var.set("") + last_mod_var.set("") + title_var.set("") + subject_var.set("") + keywords_var.set("") + category_var.set("") + comments_var.set(DEFAULT_DOC_COMMENTS) + + btns = ttk.Frame(frame) + btns.grid(row=10, column=0, columnspan=2, sticky="e", pady=(12, 0)) + ttk.Button(btns, text="Сбросить", command=_reset).pack(side=tk.LEFT, padx=(0, 8)) + ttk.Button(btns, text="Сохранить", command=_save).pack(side=tk.RIGHT) + + win.protocol("WM_DELETE_WINDOW", lambda: self._close_toplevel(win, "_win_meta")) + return win + def _create_diagrams_window(self) -> tk.Toplevel: win = tk.Toplevel(self.root) win.title("Настройки — Диаграммы") @@ -362,10 +591,10 @@ class Md2GostApp: hint = ttk.Label( extra, text=( - "Блоки ```uml / ```uml-c4 / ```bpmn / ```mermaid сами станут рисунками.\n" + "Блоки ```uml / ```uml-c4 / ```mermaid / ```idef0 / ```dfd сами станут рисунками.\n" "Схемы редактируются в меню «Шаблоны UML» (файл md2gost.schemes.json).\n" "UML: PlantUML + Java, иначе Kroki. Mermaid: системный браузер → " - "Chromium → QuickJS → Kroki (kroki.io остаётся запасным)." + "Chromium → QuickJS → Kroki. IDEF0 — Pillow. DFD — Graphviz / Kroki." ), style="Hint.TLabel", wraplength=640, @@ -377,14 +606,17 @@ class Md2GostApp: status_row.grid(row=1, column=0, columnspan=2, sticky="ew", pady=(0, 8)) status_row.columnconfigure(0, weight=1) self.diag_status = ttk.Label(status_row, text="", style="Hint.TLabel", wraplength=480, justify=tk.LEFT) - self.diag_status.grid(row=0, column=0, sticky="w", rowspan=2) + self.diag_status.grid(row=0, column=0, sticky="w", rowspan=3) btn_dl = ttk.Button(status_row, text="Скачать PlantUML", command=self._download_plantuml) btn_dl.grid(row=0, column=1, sticky="e", padx=(8, 0)) _tip(btn_dl, "Скачать plantuml.jar для локальной отрисовки (нужна Java)") + btn_gv = ttk.Button(status_row, text="Скачать Graphviz", command=self._download_graphviz) + btn_gv.grid(row=1, column=1, sticky="e", padx=(8, 0), pady=(4, 0)) + _tip(btn_gv, "Скачать portable Graphviz (dot/neato) для локальных DFD") btn_cr = ttk.Button( status_row, text="Скачать headless Chromium", command=self._download_chromium, ) - btn_cr.grid(row=1, column=1, sticky="e", padx=(8, 0), pady=(4, 0)) + btn_cr.grid(row=2, column=1, sticky="e", padx=(8, 0), pady=(4, 0)) _tip( btn_cr, "Скачать Chromium для Mermaid в %LOCALAPPDATA%\\md2gost " @@ -629,7 +861,7 @@ class Md2GostApp: def _build_schemes_help(self, parent) -> None: ttk.Label( parent, - text="Настраиваемые схемы диаграмм (uml-c4, bpmn, …)", + text="Настраиваемые схемы диаграмм (uml-c4, usecase, …)", style="Hint.TLabel", ).pack(anchor=tk.W, pady=(0, 6)) box = self._readonly_text(parent, SCHEMES_HELP.strip() + "\n") @@ -641,7 +873,7 @@ class Md2GostApp: parent, text="Скопируйте промпт и вставьте в ChatGPT / Cursor / Copilot. " "Кнопки схем дописывают макросы из md2gost.schemes.json — " - "когда ИИ должен знать C4/BPMN/свои шаблоны. Следующим сообщением — тип, тема, черновик.", + "когда ИИ должен знать C4/свои шаблоны. Следующим сообщением — тип, тема, черновик.", style="Hint.TLabel", wraplength=640, ).pack(anchor=tk.W, pady=(0, 8)) @@ -800,7 +1032,7 @@ class Md2GostApp: self.zone.pack(fill=tk.X, pady=(0, 8)) self.zone_label = tk.Label( self.zone, - text="Перетащите .md сюда · или нажмите, чтобы выбрать файл", + text="Перетащите .md или .docx сюда · или нажмите, чтобы выбрать файл", bg=ZONE, fg=NAVY, font=("Segoe UI", 11) if os.name == "nt" else ("Helvetica", 11), justify=tk.CENTER, pady=14, ) @@ -812,8 +1044,8 @@ class Md2GostApp: self.zone_path.pack(fill=tk.X, pady=(0, 8)) for w in (self.zone, self.zone_label, self.zone_path): w.bind("", lambda _e: self._browse_md()) - _tip(self.zone, "Перетащите .md или кликните, чтобы выбрать файл") - _tip(self.zone_label, "Перетащите .md или кликните, чтобы выбрать файл") + _tip(self.zone, "Перетащите .md / .docx или кликните, чтобы выбрать файл") + _tip(self.zone_label, "Перетащите .md / .docx или кликните, чтобы выбрать файл") grid = ttk.Frame(outer) grid.pack(fill=tk.X) @@ -857,6 +1089,11 @@ class Md2GostApp: checks = [ ("Заменять «—» на «-»", self.emdash_var, "Типографское тире → дефис (методичка обычно требует «—»)"), ("--- → разрыв страницы", self.hr_pagebreak_var, "Строка --- станет page break в Word"), + ( + "Автотитул", + self.auto_title_var, + "Собрать титул из info_conv.yaml / ```title (нужны ФИО и группа). По умолчанию выкл.", + ), ("Открыть после конвертации", self.open_var, "Открыть DOCX после успешной сборки"), ("Проверить по ТЗ", self.check_var, "Замечания по структуре, «рис.», источникам"), ("Только проверка", self.check_only_var, "Проверить без записи DOCX"), @@ -886,7 +1123,17 @@ class Md2GostApp: relief=tk.FLAT, padx=22, pady=7, cursor="hand2", ) self.convert_btn.pack(side=tk.LEFT) - _tip(self.convert_btn, "Собрать DOCX из выбранного markdown") + _tip(self.convert_btn, "Собрать DOCX из markdown или импортировать DOCX → Markdown") + self.import_btn = tk.Button( + actions, text="Импорт DOCX→MD", + command=self._import_docx, + bg="#3d5a40", fg="white", activebackground="#2d4430", activeforeground="white", + disabledforeground="#d0d5dc", + font=("Segoe UI", 11) if os.name == "nt" else ("Helvetica", 11), + relief=tk.FLAT, padx=14, pady=7, cursor="hand2", + ) + self.import_btn.pack(side=tk.LEFT, padx=(8, 0)) + _tip(self.import_btn, "Выбрать .docx и сохранить диалект md2gost (.md + медиа)") self.progress = ttk.Progressbar(actions, mode="indeterminate", length=180) self.progress.pack(side=tk.LEFT, padx=8, fill=tk.X, expand=True) @@ -901,7 +1148,7 @@ class Md2GostApp: self.log.configure(yscrollcommand=scroll.set) self.log.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) scroll.pack(side=tk.RIGHT, fill=tk.Y) - self.log.insert("1.0", "Готово к работе. Перетащите markdown-файл в верхнюю область.\n") + self.log.insert("1.0", "Готово к работе. Перетащите .md (сборка DOCX) или .docx (импорт в Markdown).\n") self.log.configure(state=tk.DISABLED) def _build_schemes_tab(self, parent) -> None: @@ -1067,6 +1314,7 @@ class Md2GostApp: scheme = self._scheme_from_form(old_id) if scheme is None: return None + scheme = claim_user_scheme_if_diverged(scheme) if old_id and old_id != scheme.id and old_id in self._schemes: del self._schemes[old_id] self._schemes[scheme.id] = scheme @@ -1197,7 +1445,7 @@ class Md2GostApp: if backend == "none": self._append_log("Перетаскивание недоступно — выберите файл кликом по области.") else: - self._append_log("Можно перетаскивать .md из проводника в окно.") + self._append_log("Можно перетаскивать .md / .docx из проводника в окно.") def _on_drop(self, paths: list[str]) -> None: md = first_markdown(paths) @@ -1205,8 +1453,16 @@ class Md2GostApp: self._set_markdown(md) self._flash_zone(ZONE_OK) return + docx = first_docx(paths) + if docx: + self._set_docx(docx) + self._flash_zone(ZONE_OK) + return if paths: - self._append_log("Нужен файл .md, а не: " + ", ".join(os.path.basename(p) for p in paths)) + self._append_log( + "Нужен файл .md или .docx, а не: " + + ", ".join(os.path.basename(p) for p in paths) + ) self._flash_zone("#f8d7da") def _flash_zone(self, color: str) -> None: @@ -1222,10 +1478,19 @@ class Md2GostApp: def _browse_md(self) -> None: path = filedialog.askopenfilename( - title="Исходный Markdown", - filetypes=[("Markdown", "*.md"), ("Все файлы", "*.*")], + title="Исходный Markdown или Word", + filetypes=[ + ("Markdown / Word", "*.md;*.docx"), + ("Markdown", "*.md"), + ("Word", "*.docx"), + ("Все файлы", "*.*"), + ], ) - if path: + if not path: + return + if path.lower().endswith(".docx"): + self._set_docx(path) + else: self._set_markdown(path) def _browse_output(self) -> None: @@ -1290,6 +1555,55 @@ class Md2GostApp: "Можно ничего не делать — диаграммы нарисует kroki.io.", ) + def _download_graphviz(self) -> None: + from .dfd import ( + cached_graphviz_root, + fetch_graphviz, + vendor_graphviz_root, + ) + + dest = vendor_graphviz_root() + try: + dest.mkdir(parents=True, exist_ok=True) + probe = dest / ".write_test" + probe.write_text("ok", encoding="utf-8") + probe.unlink() + except OSError: + dest = cached_graphviz_root() + self._append_log(f"Скачиваю Graphviz → {dest}") + self.root.update_idletasks() + + def work() -> None: + ok = fetch_graphviz(dest) + self.root.after(0, lambda: self._on_graphviz_installed(ok, dest)) + + import threading + + threading.Thread(target=work, daemon=True).start() + + def _on_graphviz_installed(self, ok: bool, dest) -> None: + self._refresh_diagram_status() + if ok: + engine = None + try: + from .dfd import resolve_graphviz_engine + + engine = resolve_graphviz_engine(False) + except Exception: + pass + messagebox.showinfo( + "md2gost", + f"Graphviz сохранён:\n{dest}\n\n" + f"Движок: {engine or 'dot в каталоге'}", + ) + else: + messagebox.showerror( + "md2gost", + "Не удалось скачать Graphviz.\n" + "DFD может пойти через kroki.io " + "(«Как рисовать» → через интернет) или системный PATH.", + ) + def _download_chromium(self) -> None: from .mermaid_renderer import install_playwright_chromium, playwright_browsers_dir, playwright_available @@ -1351,6 +1665,32 @@ class Md2GostApp: self._auto_output = True self._append_log(f"Файл: {path}") + def _set_docx(self, path: str) -> None: + path = os.path.abspath(path) + self._last_md = path + self.zone_label.configure(text=os.path.basename(path)) + self.zone_path.configure(text=path) + if self._auto_output: + stem = os.path.splitext(path)[0] + self.output_var.set(stem + ".md") + self._auto_output = True + self._append_log(f"DOCX для импорта: {path}") + + def _import_docx(self) -> None: + """Pick a DOCX (or use current) and run word2md.""" + if self._busy: + return + path = self._last_md if self._last_md.lower().endswith(".docx") else "" + if not path: + path = filedialog.askopenfilename( + title="Импорт DOCX → Markdown", + filetypes=[("Word", "*.docx"), ("Все файлы", "*.*")], + ) + if not path: + return + self._set_docx(path) + self._run_convert(force_import=True) + def _apply_request(self, req: ConvertRequest) -> None: self.type_var.set(_display(DOC_TYPE_LABELS, req.doc_type if req.doc_type in DOC_TYPES else "practice")) self.heading_var.set(_display(HEADING_LABELS, req.heading_numbering or DEFAULT_HEADING_NUMBERING)) @@ -1363,6 +1703,7 @@ class Md2GostApp: self.scale_var.set(str(scale).rstrip("0").rstrip(".") if isinstance(scale, float) else str(scale)) self.emdash_var.set(bool(req.emdash_to_hyphen)) self.hr_pagebreak_var.set(bool(req.hr_pagebreak)) + self.auto_title_var.set(bool(getattr(req, "auto_title", False))) self.open_var.set(True) self.check_var.set(bool(req.check)) self.check_only_var.set(bool(req.check_only)) @@ -1398,6 +1739,7 @@ class Md2GostApp: listing_continuation=_combo_key(self.listing_var.get(), LISTING_CONTINUATION_MODES, DEFAULT_LISTING_CONTINUATION), emdash_to_hyphen=self.emdash_var.get(), hr_pagebreak=self.hr_pagebreak_var.get(), + auto_title=self.auto_title_var.get(), title=self.title_var.get().strip() or None, assignment=self.assignment_var.get().strip() or None, check=self.check_var.get() or self.check_only_var.get(), @@ -1436,13 +1778,39 @@ class Md2GostApp: return value if value >= 1 else 1.0 def _convert(self) -> None: + self._run_convert(force_import=False) + + def _run_convert(self, *, force_import: bool) -> None: if self._busy: return req = self._collect() if not req.filename: - messagebox.showinfo("md2gost", "Сначала перетащите или выберите markdown-файл.") + messagebox.showinfo( + "md2gost", + "Сначала перетащите или выберите .md (сборка) или .docx (импорт).", + ) return - if not req.check_only: + is_docx = req.filename.lower().endswith(".docx") + if force_import and not is_docx: + messagebox.showinfo("md2gost", "Для импорта нужен файл .docx.") + return + if is_docx: + target = (req.output or "").strip() + if target.lower().endswith(".docx"): + target = os.path.splitext(req.filename)[0] + ".md" + req.output = target + self.output_var.set(target) + if target and os.path.isfile(target): + if not messagebox.askyesno( + "md2gost", f"Файл уже есть:\n{target}\n\nПерезаписать?" + ): + base, ext = os.path.splitext(target) + target = f"{base}_import{ext or '.md'}" + self._append_log(f"Не перезаписываем, пишем в {target}") + req.output = target + self.output_var.set(target) + self._append_log("Импорт DOCX → Markdown…") + elif not req.check_only: target = (req.output or "").strip() or ( default_output_path(req.filename) if req.filename else "" ) @@ -1455,10 +1823,15 @@ class Md2GostApp: req.output = target self.output_var.set(target) self._auto_output = False + self._append_log("Конвертация…") + else: + self._append_log("Проверка…") + self._busy = True self.convert_btn.configure(state=tk.DISABLED, bg=NAVY_DIM) + if hasattr(self, "import_btn"): + self.import_btn.configure(state=tk.DISABLED) self.progress.start(12) - self._append_log("Конвертация…") def work(): result = convert(req, log=lambda m: self.root.after(0, self._append_log, m)) @@ -1469,6 +1842,8 @@ class Md2GostApp: def _on_done(self, result) -> None: self._busy = False self.convert_btn.configure(state=tk.NORMAL, bg=NAVY) + if hasattr(self, "import_btn"): + self.import_btn.configure(state=tk.NORMAL, bg="#3d5a40") self.progress.stop() if self._debug_next or self.debug_var.get(): self.debug_var.set(False) diff --git a/md2gost/help_content.py b/md2gost/help_content.py index e191f7b..48d9d97 100644 --- a/md2gost/help_content.py +++ b/md2gost/help_content.py @@ -13,6 +13,8 @@ USAGE_HELP = """md2gost — Markdown → DOCX (ТЗ МИРЭА / ГОСТ 7.32) 1. Перетащите .md в верхнюю область (или кликните по ней). 2. Выберите тип документа и параметры в блоке «Основные». Шаблон / титул / задание — Настройки → Файлы. + ФИО и группа студента — Настройки → Студент… (спрашивают при первом запуске). + Свойства DOCX (автор, название, примечание) — Настройки → Метаданные…. PlantUML / Mermaid / Kroki — Настройки → Диаграммы. Свои UML-схемы — меню «Шаблоны UML». 3. Нажмите «Конвертировать». Документ сохранится рядом с исходником (или по пути «Выходной DOCX»). @@ -28,14 +30,29 @@ USAGE_HELP = """md2gost — Markdown → DOCX (ТЗ МИРЭА / ГОСТ 7.32) • Проверить по ТЗ — замечания по структуре, «рис.», источникам. • Проверить вёрстку в 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». Меняет поля страницы и параметры @@ -101,12 +118,32 @@ CLI (тот же движок) 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, или uml- / из файла схем (c4, usecase, bpmn, …). -BPMN 2.0: оградка ```bpmn / ```uml-bpmn, макросы Start, UserTask, XOR, Flow, Pool. -Формат в Word: PNG по умолчанию (PlantUML и локальный Mermaid ~2× для чёткости); --diagram-format svg — вектор + PNG-запасной (Word 2016+). -IDEF0 конвертер не рисует — вставляйте готовый PNG как обычный Рисунок. +Языки: uml, plantuml, mermaid / mmd, idef0, dfd / data-flow-diagram, или uml- / из файла схем (c4, usecase, archimate, …). +Формат в Word: PNG по умолчанию (PlantUML, локальный Mermaid, IDEF0 и DFD ~2× для чёткости); --diagram-format svg — вектор + PNG-запасной (Word 2016+). Подробнее — Справка → Схемы и раздел ниже в инструкции. Формула (номер только если есть ссылка) @@ -129,13 +166,17 @@ IDEF0 конвертер не рисует — вставляйте готовы native — поле Word (обновить при открытии); manual — собирает md2gost. Продолжение таблиц / листингов - word — после сборки Word COM режет по реальной пагинации и вставляет «Продолжение…» - (по умолчанию; нужны Windows + Word + pywin32). - off — не резать, Word сам переносит. + word — по умолчанию: после сборки Word COM режет по реальной пагинации и + вставляет «Продолжение…» (нужны Windows + Word + pywin32). Также уводит + таблицу целиком, если внизу страницы зависли только название или + название + шапка граф (методичка: рис. Г.3). + off — одна таблица, Word сам переносит строки; без авто«Продолжение». + Защита от сирот: keep_with_next у подписи/шапки и оценка + «подпись+шапка+1 строка» при рендере (без COM слабее, чем word). legacy / caption — режем по оценке высоты в md2gost (может не совпасть с Word). Промпт для ИИ — Справка → Промпт для ИИ: выберите промпт, при необходимости -включите схемы (C4, BPMN, …) кнопками — макросы допишутся в конец — скопируйте +включите схемы (C4, usecase, …) кнопками — макросы допишутся в конец — скопируйте в ChatGPT / Cursor / Copilot, затем дайте тему и черновик. Документация — Справка → Документация: встроенный просмотр docs/*.md @@ -151,15 +192,22 @@ SCHEMES_HELP = """СХЕМЫ ДИАГРАММ (PlantUML) Первый запуск Рядом с программой (рядом с 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» - ```bpmn / ```uml-bpmn — BPMN 2.0 (макросы в diagrams/BPMN.puml) + ```uml-archimate — ArchiMate 3.2 (макросы Business_*, Rel_Serving, …) ```mermaid / ```mmd — Mermaid локально (браузер / Chromium / QuickJS) или через Kroki -Встроенные пресеты PlantUML: c4, c4context, c4component, usecase, bpmn. + ```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. Пример @@ -171,18 +219,6 @@ System(app, "Портал") Rel(user, app, "логин") ``` -```bpmn -StartMessage(s, "заявка") -UserTask(t, "Проверить") -XOR(gw) -End(e_ok) -End(e_no) -Flow(s, t) -Flow(t, gw) -CondFlow(gw, e_ok, "да") -DefaultFlow(gw, e_no) -``` - Поля схемы в JSON title — подпись в GUI version — версия схемы @@ -207,27 +243,17 @@ URL из includes (и !include https://… внутри .puml) при перво «Сбросить кэш includes» в Настройки → Диаграммы или в «Шаблоны UML» удаляет индекс и только файлы, перечисленные в нём. -BPMN 2.0 (```bpmn) - Пул: Pool(alias, "Участник") { Lane(alias, "Роль") { … } } - События: Start, StartMessage, StartTimer, CatchMessage, ThrowMessage, - CatchTimer, CatchError, End, EndMessage, EndError, EndTerminate - Задачи: Task / UserTask / ServiceTask / ScriptTask / ManualTask / - SendTask / ReceiveTask / BusinessRuleTask / SubProcess - Шлюзы: XOR (Exclusive), AND (Parallel), OR (Inclusive), EventBased - Потоки: Flow, CondFlow(from, to, "условие"), DefaultFlow, MessageFlow - Данные: DataObject, DataStore, Annotation - Граница: BoundaryError/Timer/Message + Attach(task, event) - Sequence Flow только внутри пула; между пулами — MessageFlow. - Чего нет - IDEF0 / DFD — PlantUML не умеет; вставляйте готовый PNG как Рисунок. - У BPMN нет «прилипания» boundary-события к кромке задачи (ставьте Attach) - и нет настоящей двойной окружности у intermediate (толщина линии). + Балансировка родитель/потомок 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 у %подписи — альбомная страница под широкий рисунок/таблицу. """ @@ -236,6 +262,7 @@ PROMPT_FILES = ( ("generate-md.md", "Markdown для md2gost"), ("generate-mirea-report.md", "МИРЭА / ГОСТ (курсовая, практика, ВКР)"), ("generate-pis-custom-report.md", "ПИС — отчёт по практическим работам"), + ("emulate-student.md", "Стиль: человечный текст, не отчёт ИИ"), ) diff --git a/md2gost/idef0.py b/md2gost/idef0.py new file mode 100644 index 0000000..bf2005d --- /dev/null +++ b/md2gost/idef0.py @@ -0,0 +1,2185 @@ +"""IDEF0 diagrams from a simple text DSL (FIPS 183 / IEEE 1320.1). + +Fence language: ```idef0 (alias ```uml-idef0). +Rendered locally with Pillow — no PlantUML / Kroki. +""" + +from __future__ import annotations + +import logging +import math +import os +import re +import xml.sax.saxutils +from dataclasses import dataclass, field +from pathlib import Path +from PIL import Image, ImageDraw, ImageFont + +_log = logging.getLogger(__name__) + +CACHE_KEY_PREFIX = "idef0-v6\n" + +# --- model ----------------------------------------------------------------- + +Side = str # "I" | "C" | "O" | "M" | "CALL" +_SIDES = frozenset({"I", "C", "O", "M", "CALL"}) + +_SIDE_FROM_TOKEN = { + "i": "I", + "in": "I", + "input": "I", + "c": "C", + "ctrl": "C", + "control": "C", + "^": "C", + "o": "O", + "out": "O", + "output": "O", + "m": "M", + "mech": "M", + "mechanism": "M", + "v": "M", + "call": "CALL", +} + +_STATUS_CANON = { + "working": "WORKING", + "draft": "DRAFT", + "recommended": "RECOMMENDED", + "publication": "PUBLICATION", + "рабочий": "WORKING", + "черновик": "DRAFT", + "рекомендован": "RECOMMENDED", + "рекомендовано": "RECOMMENDED", + "публикация": "PUBLICATION", +} + +_STATUSES = ("WORKING", "DRAFT", "RECOMMENDED", "PUBLICATION") + +# Longer keys first. +_META_ALIASES: list[tuple[str, str]] = [ + ("used at", "used_at"), + ("used_at", "used_at"), + ("used-at", "used_at"), + ("reader date", "reader_date"), + ("reader_date", "reader_date"), + ("reader-date", "reader_date"), + ("author", "author"), + ("project", "project"), + ("date", "date"), + ("revision", "rev"), + ("rev", "rev"), + ("status", "status"), + ("context", "context"), + ("node", "node"), + ("title", "title"), + ("number", "number"), + ("notes", "notes"), + ("reader", "reader"), + ("purpose", "purpose"), + ("viewpoint", "viewpoint"), + ("page", "page"), + ("form", "form"), + ("автор", "author"), + ("проект", "project"), + ("дата", "date"), + ("узел", "node"), + ("название", "title"), + ("номер", "number"), + ("статус", "status"), + ("контекст", "context"), + ("цель", "purpose"), + ("точка зрения", "viewpoint"), +] + +_BOX_ID_RE = re.compile(r"^[A-Za-z][A-Za-z0-9_-]*$") +_A_NODE_RE = re.compile(r"^A-?\d+[A-Za-z0-9]*$", re.I) +_BOX_DEF_BRACKET = re.compile( + r"^\[([A-Za-z][A-Za-z0-9_-]*)\]\s*(.*)$", +) +_BOX_DEF_KW = re.compile( + r"^box\s+([A-Za-z][A-Za-z0-9_-]*)\s*(.*)$", + re.I, +) +_NOT_BOX_REST = re.compile( + r"^(?:in|input|out|output|ctrl|control|mech|mechanism|call|[ICOM])\b|^<-|^->|^\^|^v\b", + re.I, +) +_ICOM_TAIL = re.compile(r"\[([ICOM]\d+)\]\s*$", re.I) +_KW_ARROW = re.compile( + r"^(\(?)\s*(?:([A-Za-z][A-Za-z0-9_-]*)\s+)?" + r"(\(?)(in|input|out|output|ctrl|control|mech|mechanism|call|[ICOM])\b(\)?)" + r"\s*(.*)$", + re.I, +) + + +class Idef0ParseError(ValueError): + """Invalid IDEF0 DSL.""" + + +@dataclass +class Idef0Box: + id: str + name: str + number: str = "" # lower-right corner; default = id + + +@dataclass +class Idef0End: + """One end of an arrow: a box port, or the diagram boundary.""" + + box: str | None + side: Side + tunnel: bool = False + + +@dataclass +class Idef0Arrow: + sources: list[Idef0End] + targets: list[Idef0End] + label: str = "" + icom: str = "" # e.g. I1, C2 — drawn near the unconnected end + + +@dataclass +class Idef0Diagram: + title: str = "" + node: str = "" + number: str = "1" + author: str = "" + project: str = "" + date: str = "" + rev: str = "" + status: str = "" + context: str = "" + used_at: str = "" + reader: str = "" + reader_date: str = "" + notes: str = "" + purpose: str = "" + viewpoint: str = "" + page: str = "" + form: str = "kit" # kit | plain + boxes: list[Idef0Box] = field(default_factory=list) + arrows: list[Idef0Arrow] = field(default_factory=list) + + def box_ids(self) -> dict[str, Idef0Box]: + return {b.id: b for b in self.boxes} + + +# --- parse ----------------------------------------------------------------- + +def _strip_comment(line: str) -> str: + if line.lstrip().startswith("#"): + return "" + return line + + +def _unquote(text: str) -> str: + text = text.strip() + if len(text) >= 2 and text[0] == text[-1] and text[0] in "\"'": + return text[1:-1].strip() + return text + + +def _match_meta(line: str) -> tuple[str, str] | None: + raw = line.strip() + low = raw.lower() + for prefix, key in _META_ALIASES: + if not low.startswith(prefix): + continue + rest = raw[len(prefix) :] + if rest == "": + return key, "" + if rest[0] in ":\t ": + return key, _unquote(rest.lstrip(":\t ")) + return None + + +def _looks_like_box_id(name: str, known: set[str]) -> bool: + if name in known: + return True + if _A_NODE_RE.match(name): + return True + if re.match(r"^[1-6]$", name): + return True + return False + + +def _parse_side_token(token: str | None, default: Side) -> Side: + if not token: + return default + key = token.strip().lower().lstrip(".") + if key in _SIDE_FROM_TOKEN: + return _SIDE_FROM_TOKEN[key] + if key.upper() in _SIDES: + return key.upper() # type: ignore[return-value] + return default + + +def _parse_end_token(token: str, default_side: Side, known: set[str]) -> Idef0End | None: + text = token.strip() + if not text: + return None + tunnel = False + if text.startswith("(") and text.endswith(")") and len(text) > 2: + tunnel = True + text = text[1:-1].strip() + elif text.startswith("("): + tunnel = True + text = text[1:].strip() + elif text.endswith(")"): + tunnel = True + text = text[:-1].strip() + m = re.match( + r"^([A-Za-z][A-Za-z0-9_-]*)(?:\.([A-Za-z]+))?$", + text, + ) + if not m or not _looks_like_box_id(m.group(1), known): + return None + return Idef0End(box=m.group(1), side=_parse_side_token(m.group(2), default_side), tunnel=tunnel) + + +def _consume_ends( + text: str, + default_side: Side, + known: set[str], +) -> tuple[list[Idef0End], str]: + """Parse a comma-separated box-ref list from the start of text.""" + rest = text.strip() + ends: list[Idef0End] = [] + while rest: + m = re.match( + r"^\s*(\(?[A-Za-z][A-Za-z0-9_-]*(?:\.[A-Za-z]+)?\)?)\s*(,\s*)?", + rest, + ) + if not m: + break + end = _parse_end_token(m.group(1), default_side, known) + if end is None: + break + ends.append(end) + rest = rest[m.end() :] + if not m.group(2): + # no comma — only continue if next token is clearly another ref + break + return ends, rest.strip() + + +def _split_label_icom(text: str) -> tuple[str, str]: + text = text.strip() + if text.startswith(":"): + text = text[1:].strip() + icom = "" + m = _ICOM_TAIL.search(text) + if m: + icom = m.group(1).upper() + text = text[: m.start()].strip() + return _unquote(text), icom + + +def _split_arrow_op(line: str) -> tuple[str, str, str] | None: + for op in ("<-", "-->", "->"): + i = line.find(op) + if i >= 0: + return line[:i], op, line[i + len(op) :] + parts = line.split() + for idx, tok in enumerate(parts): + if tok in ("^", "v", "V"): + left = " ".join(parts[:idx]) + right = " ".join(parts[idx + 1 :]) + return left, tok, right + return None + + +def _ensure_box(diagram: Idef0Diagram, box_id: str, name: str = "") -> None: + ids = {b.id for b in diagram.boxes} + if box_id in ids: + if name: + for b in diagram.boxes: + if b.id == box_id and not b.name: + b.name = name + return + diagram.boxes.append(Idef0Box(id=box_id, name=name or box_id, number=box_id)) + + +def _current_box_id(diagram: Idef0Diagram) -> str: + if diagram.boxes: + return diagram.boxes[-1].id + return "A0" + + +def _boundary_end(side: Side, tunnel: bool = False) -> Idef0End: + return Idef0End(box=None, side=side, tunnel=tunnel) + + +def _parse_arrow_line(line: str, diagram: Idef0Diagram, lineno: int) -> None: + known = {b.id for b in diagram.boxes} + raw = line.strip() + tun_border = False + tun_box = False + work = raw + if work.startswith("(") and not work.startswith("(A") and not re.match(r"^\([A-Za-z]", work): + tun_border = True + work = work[1:].lstrip() + # keyword form: [box] in/out/ctrl/mech/call label + km = _KW_ARROW.match(work) + if km: + tun_kw_b = bool(km.group(1) or km.group(3)) + box_id = km.group(2) or _current_box_id(diagram) + kw = km.group(4) + tun_kw_box = bool(km.group(5)) + rest = km.group(6) + side = _parse_side_token(kw, "I") + label, icom = _split_label_icom(rest) + _ensure_box(diagram, box_id) + box_end = Idef0End(box=box_id, side="M" if side == "CALL" else side, tunnel=tun_kw_box or tun_box) + if side in ("I", "C", "M"): + diagram.arrows.append( + Idef0Arrow( + sources=[_boundary_end(side, tun_kw_b or tun_border)], + targets=[box_end], + label=label, + icom=icom, + ) + ) + else: # O or CALL — leave the box + out_side: Side = "CALL" if side == "CALL" else "O" + box_end.side = out_side + diagram.arrows.append( + Idef0Arrow( + sources=[box_end], + targets=[_boundary_end(out_side, tun_kw_b or tun_border)], + label=label, + icom=icom, + ) + ) + return + + split = _split_arrow_op(work) + if not split: + raise Idef0ParseError(f"IDEF0: не удалось разобрать строку {lineno}: {raw}") + left, op, right = split + left, right = left.strip(), right.strip() + if left.endswith("("): + tun_border = True + left = left[:-1].strip() + if right.startswith(")"): + tun_box = True + right = right[1:].strip() + if op in ("<-",): + side: Side = "I" + elif op in ("^",): + side = "C" + elif op in ("v", "V"): + side = "M" + else: + side = "O" + + if side == "O": + srcs, left_rest = _consume_ends(left, "O", known) + if left_rest: + # leftover on the left is unexpected for output + if not srcs: + raise Idef0ParseError(f"IDEF0: не удалось разобрать строку {lineno}: {raw}") + if not srcs: + bid = _current_box_id(diagram) + _ensure_box(diagram, bid) + srcs = [Idef0End(box=bid, side="O", tunnel=tun_box)] + else: + for e in srcs: + if e.side == "O" and "." not in (left or ""): + e.side = "O" + e.tunnel = e.tunnel or tun_box + for e in srcs: + if e.box: + _ensure_box(diagram, e.box) + + tgts, tgt_rest = _consume_ends(right, "I", known) + if tgts: + label, icom = _split_label_icom(tgt_rest) + for t in tgts: + if t.box: + _ensure_box(diagram, t.box) + t.tunnel = t.tunnel or tun_border + diagram.arrows.append(Idef0Arrow(sources=srcs, targets=tgts, label=label, icom=icom)) + return + label, icom = _split_label_icom(right) + diagram.arrows.append( + Idef0Arrow( + sources=srcs, + targets=[_boundary_end("O", tun_border)], + label=label, + icom=icom, + ) + ) + return + + # Input / control / mechanism into a box (from boundary or from other boxes) + srcs, src_rest = _consume_ends(left, "O", known) + if srcs and not src_rest: + # A1 -> already handled; here A1 ^ or "A2, A3 <-" wait, left of <- is the box + pass + + if op == "<-": + # `A1, A2 <- X` is unusual; `A1 <- label` or `A2 <- A1` (A1 output into A2) + # If right starts with box refs: internal into this side + box_left, left_rest = _consume_ends(left or "", "I", known) + if not box_left: + bid = _current_box_id(diagram) + _ensure_box(diagram, bid) + box_left = [Idef0End(box=bid, side="I", tunnel=tun_box)] + else: + for e in box_left: + e.side = "I" + e.tunnel = e.tunnel or tun_box + if e.box: + _ensure_box(diagram, e.box) + from_boxes, from_rest = _consume_ends(right, "O", known) + if from_boxes: + label, icom = _split_label_icom(from_rest) + for e in from_boxes: + e.side = "O" + if e.box: + _ensure_box(diagram, e.box) + diagram.arrows.append( + Idef0Arrow(sources=from_boxes, targets=box_left, label=label, icom=icom) + ) + return + label, icom = _split_label_icom(right) + diagram.arrows.append( + Idef0Arrow( + sources=[_boundary_end("I", tun_border)], + targets=box_left, + label=label, + icom=icom, + ) + ) + return + + # ^ or v : `A1 ^ label` or `A1 ^ A2` (A2 as source? rare) — treat right as label + # unless right is box refs (output of those boxes feeding this control/mech) + box_left, _ = _consume_ends(left or "", side, known) + if not box_left: + bid = _current_box_id(diagram) + _ensure_box(diagram, bid) + box_left = [Idef0End(box=bid, side=side, tunnel=tun_box)] + else: + for e in box_left: + e.side = side + e.tunnel = e.tunnel or tun_box + if e.box: + _ensure_box(diagram, e.box) + from_boxes, from_rest = _consume_ends(right, "O", known) + if from_boxes: + label, icom = _split_label_icom(from_rest) + for e in from_boxes: + e.side = "O" + if e.box: + _ensure_box(diagram, e.box) + diagram.arrows.append( + Idef0Arrow(sources=from_boxes, targets=box_left, label=label, icom=icom) + ) + return + label, icom = _split_label_icom(right) + diagram.arrows.append( + Idef0Arrow( + sources=[_boundary_end(side, tun_border)], + targets=box_left, + label=label, + icom=icom, + ) + ) + + +def _parse_box_line(line: str) -> Idef0Box | None: + m = _BOX_DEF_BRACKET.match(line) + if m: + return Idef0Box(id=m.group(1), name=_unquote(m.group(2)) or m.group(1), number=m.group(1)) + m = _BOX_DEF_KW.match(line) + if m: + return Idef0Box(id=m.group(1), name=_unquote(m.group(2)) or m.group(1), number=m.group(1)) + # Shorthand: A0 Name (must look like an IDEF0 node id) + m = re.match(r"^(A-?\d+[A-Za-z0-9]*)\s+(.+)$", line) + if m and "->" not in line and "<-" not in line: + rest = m.group(2).strip() + if rest and rest[0] not in "^vV" and not _NOT_BOX_REST.match(rest) and not _KW_ARROW.match(line): + if rest.startswith("(") or rest.startswith(")"): + return None + return Idef0Box(id=m.group(1), name=_unquote(rest), number=m.group(1)) + return None + + +def _canon_status(value: str) -> str: + token = value.strip().split()[0] if value.strip() else "" + return _STATUS_CANON.get(token.lower(), token.upper()) + + +def parse_idef0(source: str) -> Idef0Diagram: + """Parse an ```idef0 body into a diagram model.""" + diagram = Idef0Diagram() + pending: list[tuple[int, str]] = [] + saw_content = False + if not (source or "").strip(): + raise Idef0ParseError("IDEF0: пустой блок") + + for lineno, raw in enumerate(source.splitlines(), 1): + line = _strip_comment(raw).strip() + if not line: + continue + saw_content = True + meta = _match_meta(line) + if meta: + key, val = meta + if key == "status": + val = _canon_status(val) + if hasattr(diagram, key): + setattr(diagram, key, val) + continue + box = _parse_box_line(line) + if box: + _ensure_box(diagram, box.id, box.name) + for b in diagram.boxes: + if b.id == box.id: + b.name = box.name or b.name + b.number = box.number or b.number + continue + pending.append((lineno, line)) + + if not saw_content: + raise Idef0ParseError("IDEF0: пустой блок") + + if not diagram.boxes: + _ensure_box(diagram, "A0", diagram.title or "A0") + + for lineno, line in pending: + _parse_arrow_line(line, diagram, lineno) + + for arrow in diagram.arrows: + if not re.match(r"^O\d+$", arrow.icom or "", re.I): + continue + has_c = any(t.box and t.side == "C" for t in arrow.targets) + has_bound = any(t.box is None for t in arrow.targets) + if has_c and not has_bound: + arrow.targets.append(_boundary_end("O")) + + if not diagram.node: + if len(diagram.boxes) == 1 and diagram.boxes[0].id.upper() in {"A0", "A-0", "0"}: + diagram.node = "A-0" + else: + diagram.node = "A0" + if not diagram.title and diagram.boxes: + diagram.title = diagram.boxes[0].name + if not diagram.context and diagram.node.upper() in {"A-0", "A0"}: + if diagram.node.upper() == "A-0": + diagram.context = "TOP" + if not diagram.page: + diagram.page = diagram.number or "1" + return diagram + + +# --- fonts / geometry ------------------------------------------------------ + +def _font_candidates(bold: bool) -> list[Path]: + names_win = ( + ("arialbd.ttf", "segoeuib.ttf", "calibrib.ttf") + if bold + else ("arial.ttf", "segoeui.ttf", "calibri.ttf") + ) + names_unix = ( + ("DejaVuSans-Bold.ttf", "LiberationSans-Bold.ttf", "FreeSansBold.ttf") + if bold + else ("DejaVuSans.ttf", "LiberationSans-Regular.ttf", "FreeSans.ttf") + ) + out: list[Path] = [] + windir = os.environ.get("WINDIR") or r"C:\Windows" + fonts_win = Path(windir) / "Fonts" + for n in names_win: + out.append(fonts_win / n) + for root in ( + Path("/usr/share/fonts/truetype/dejavu"), + Path("/usr/share/fonts/truetype/liberation"), + Path("/usr/share/fonts/truetype/freefont"), + Path("/usr/share/fonts/TTF"), + Path("/usr/share/fonts/truetype/msttcorefonts"), + ): + for n in names_unix: + out.append(root / n) + return out + + +_FONT_CACHE: dict[tuple[int, bool], ImageFont.ImageFont] = {} + + +def _load_font(size: int, bold: bool = False) -> ImageFont.ImageFont: + size = max(8, int(size)) + key = (size, bold) + hit = _FONT_CACHE.get(key) + if hit is not None: + return hit + for path in _font_candidates(bold): + if path.is_file(): + try: + font = ImageFont.truetype(str(path), size=size) + _FONT_CACHE[key] = font + return font + except OSError: + continue + font = ImageFont.load_default() + _FONT_CACHE[key] = font + return font + + +def _text_wh(font: ImageFont.ImageFont, text: str) -> tuple[int, int]: + if not text: + return 0, 0 + if hasattr(font, "getbbox"): + l, t, r, b = font.getbbox(text) + return max(0, r - l), max(0, b - t) + if hasattr(font, "getsize"): + w, h = font.getsize(text) # type: ignore[attr-defined] + return int(w), int(h) + return len(text) * 8, 12 + + +def _wrap(text: str, font: ImageFont.ImageFont, max_width: int) -> list[str]: + text = (text or "").strip() + if not text: + return [""] + words = text.split() + lines: list[str] = [] + cur = "" + for word in words: + trial = word if not cur else f"{cur} {word}" + if _text_wh(font, trial)[0] <= max_width or not cur: + cur = trial + else: + lines.append(cur) + cur = word + if cur: + lines.append(cur) + return lines or [""] + + +# --- drawing ops ----------------------------------------------------------- + +@dataclass +class _Rect: + x: float + y: float + w: float + h: float + + @property + def r(self) -> float: + return self.x + self.w + + @property + def b(self) -> float: + return self.y + self.h + + @property + def cx(self) -> float: + return self.x + self.w / 2 + + @property + def cy(self) -> float: + return self.y + self.h / 2 + + def contains_point(self, px: float, py: float, pad: float = 0.0) -> bool: + return self.x - pad <= px <= self.r + pad and self.y - pad <= py <= self.b + pad + + def inflate(self, p: float) -> "_Rect": + return _Rect(self.x - p, self.y - p, self.w + 2 * p, self.h + 2 * p) + + def overlap_area(self, other: "_Rect") -> float: + ix = max(0.0, min(self.r, other.r) - max(self.x, other.x)) + iy = max(0.0, min(self.b, other.b) - max(self.y, other.y)) + return ix * iy + + +@dataclass +class _BoxGeom: + box: Idef0Box + rect: _Rect + slots: dict[str, list[tuple[int, Idef0Arrow, Idef0End]]] = field(default_factory=dict) + + +class _Scene: + def __init__(self, width: int, height: int): + self.width = width + self.height = height + self.ops: list[tuple] = [] + + def line(self, x1, y1, x2, y2, width: float = 1.5) -> None: + self.ops.append(("line", float(x1), float(y1), float(x2), float(y2), float(width))) + + def polyline(self, pts: list[tuple[float, float]], width: float = 1.5) -> None: + if len(pts) < 2: + return + self.ops.append(("polyline", [(float(x), float(y)) for x, y in pts], float(width))) + + def rect( + self, + x, + y, + w, + h, + width: float = 1.5, + fill: str | None = None, + outline: str | None = "#000000", + ) -> None: + self.ops.append( + ("rect", float(x), float(y), float(w), float(h), float(width), fill, outline) + ) + + def polygon( + self, + pts: list[tuple[float, float]], + fill: str = "#000000", + outline: str = "#000000", + width: float = 1, + ) -> None: + self.ops.append( + ("polygon", [(float(x), float(y)) for x, y in pts], fill, outline, float(width)) + ) + + def text( + self, + x, + y, + text: str, + size: float, + *, + bold: bool = False, + anchor: str = "lt", + fill: str = "#000000", + ) -> None: + if not text: + return + self.ops.append(("text", float(x), float(y), text, float(size), bold, anchor, fill)) + + def arc(self, bbox: tuple[float, float, float, float], start: float, end: float, width: float) -> None: + self.ops.append(("arc", tuple(float(v) for v in bbox), float(start), float(end), float(width))) + + def to_png(self) -> bytes: + img = Image.new("RGB", (self.width, self.height), "#ffffff") + draw = ImageDraw.Draw(img) + for op in self.ops: + kind = op[0] + if kind == "line": + _, x1, y1, x2, y2, w = op + draw.line([(x1, y1), (x2, y2)], fill="#000000", width=max(1, int(round(w)))) + elif kind == "polyline": + _, pts, w = op + draw.line(pts, fill="#000000", width=max(1, int(round(w)))) + elif kind == "rect": + _, x, y, w, h, lw, fill, outline = op + xy = [x, y, x + w, y + h] + stroke_w = max(1, int(round(lw))) if outline else 0 + draw.rectangle(xy, fill=fill, outline=outline, width=stroke_w) + elif kind == "polygon": + _, pts, fill, outline, w = op + draw.polygon(pts, fill=fill, outline=outline) + elif kind == "text": + _, x, y, text, size, bold, anchor, fill = op + font = _load_font(int(round(size)), bold) + draw.text((x, y), text, font=font, fill=fill, anchor=anchor) + elif kind == "arc": + _, bbox, start, end, w = op + draw.arc(bbox, start, end, fill="#000000", width=max(1, int(round(w)))) + from io import BytesIO + + buf = BytesIO() + img.save(buf, format="PNG", optimize=True) + return buf.getvalue() + + def to_svg(self) -> str: + esc = xml.sax.saxutils.escape + parts = [ + f'', + '', + ] + for op in self.ops: + kind = op[0] + if kind == "line": + _, x1, y1, x2, y2, w = op + parts.append( + f'' + ) + elif kind == "polyline": + _, pts, w = op + d = " ".join(f"{x:.1f},{y:.1f}" for x, y in pts) + parts.append( + f'' + ) + elif kind == "rect": + _, x, y, w, h, lw, fill, outline = op + fill_a = fill or "none" + stroke = outline or "none" + sw = 0 if outline is None else lw + parts.append( + f'' + ) + elif kind == "polygon": + _, pts, fill, outline, w = op + d = " ".join(f"{x:.1f},{y:.1f}" for x, y in pts) + parts.append( + f'' + ) + elif kind == "text": + _, x, y, text, size, bold, anchor, fill = op + ta, dx, dy = _svg_anchor(anchor) + weight = "700" if bold else "400" + parts.append( + f'' + f"{esc(text)}" + ) + elif kind == "arc": + _, bbox, start, end, w = op + parts.append(_svg_arc(bbox, start, end, w)) + parts.append("") + return "\n".join(parts) + "\n" + + +def _svg_anchor(anchor: str) -> tuple[str, str, str]: + """Map PIL anchors to SVG text-anchor + baseline tweak.""" + a = (anchor or "lt").lower() + horiz = a[0] if a else "l" + vert = a[1] if len(a) > 1 else "t" + ta = {"l": "start", "m": "middle", "r": "end"}.get(horiz, "start") + # PIL anchor is the point; SVG y is alphabetic baseline ≈ 0.8 em from top + dy = {"t": "0.9em", "m": "0.35em", "b": "0"}.get(vert, "0.9em") + return ta, "0", dy + + +def _svg_arc(bbox, start: float, end: float, width: float) -> str: + x0, y0, x1, y1 = bbox + cx, cy = (x0 + x1) / 2, (y0 + y1) / 2 + rx, ry = abs(x1 - x0) / 2, abs(y1 - y0) / 2 + import math + + def pt(deg: float) -> tuple[float, float]: + rad = math.radians(deg) + return cx + rx * math.cos(rad), cy + ry * math.sin(rad) + + sx, sy = pt(start) + ex, ey = pt(end) + large = 1 if abs(end - start) % 360 > 180 else 0 + return ( + f'' + ) + + +# --- layout + render ------------------------------------------------------- + +def _arrowhead(scene: _Scene, tip: tuple[float, float], direction: str, size: float, lw: float) -> None: + """direction = where the arrow points: E W N S.""" + x, y = tip + s = size + if direction == "E": + pts = [(x, y), (x - s, y - s * 0.42), (x - s, y + s * 0.42)] + elif direction == "W": + pts = [(x, y), (x + s, y - s * 0.42), (x + s, y + s * 0.42)] + elif direction == "S": + pts = [(x, y), (x - s * 0.42, y - s), (x + s * 0.42, y - s)] + else: + pts = [(x, y), (x - s * 0.42, y + s), (x + s * 0.42, y + s)] + scene.polygon(pts, fill="#000000", outline="#000000", width=lw) + + +def _dir_for_into(side: Side) -> str: + return {"I": "E", "C": "S", "O": "E", "M": "N", "CALL": "S"}[side] + + +def _dir_for_out(side: Side) -> str: + return {"I": "W", "C": "N", "O": "E", "M": "S", "CALL": "S"}[side] + + +def _stub_dir_from_box(side: Side, leaving: bool = True) -> str: + """Outward from a box face (first/last elbow of the route).""" + return {"I": "W", "C": "N", "O": "E", "M": "S", "CALL": "S"}[side] + + +def _end_stub_dir(end: Idef0End) -> str: + """Direction from the endpoint toward the interior of the route.""" + if end.box: + return _stub_dir_from_box(end.side) + # Boundary: step into the diagram, not into the title block. + return {"I": "E", "C": "S", "O": "W", "M": "N", "CALL": "N"}[end.side] + + +def _offset(pt: tuple[float, float], direction: str, dist: float) -> tuple[float, float]: + x, y = pt + if direction == "E": + return x + dist, y + if direction == "W": + return x - dist, y + if direction == "N": + return x, y - dist + return x, y + dist + + +def _port_point(geom: _BoxGeom, side: Side, index: int, count: int) -> tuple[float, float]: + r = geom.rect + n = max(count, 1) + if n == 1: + t = 0.5 + else: + pad = min(0.22, 0.48 / n) + t = pad + (1.0 - 2.0 * pad) * (index / (n - 1)) + if side in ("I",): + return r.x, r.y + r.h * t + if side in ("O",): + return r.r, r.y + r.h * t + if side in ("C",): + return r.x + r.w * t, r.y + return r.x + r.w * t, r.b + + +def _boundary_point(work: _Rect, side: Side, along: float) -> tuple[float, float]: + if side == "I": + return work.x, along + if side == "O": + return work.r, along + if side == "C": + return along, work.y + if side == "CALL": + return along, work.b + return along, work.b + + +def _dedupe_pts(pts: list[tuple[float, float]]) -> list[tuple[float, float]]: + out: list[tuple[float, float]] = [] + for p in pts: + if not out or abs(out[-1][0] - p[0]) > 0.8 or abs(out[-1][1] - p[1]) > 0.8: + out.append(p) + return out + + +def _simplify_ortho(pts: list[tuple[float, float]]) -> list[tuple[float, float]]: + """Dedupe, merge colinear runs, drop overshoot/backtracking on the same axis.""" + pts = _dedupe_pts(pts) + if len(pts) < 3: + return pts + merged: list[tuple[float, float]] = [pts[0]] + for i in range(1, len(pts) - 1): + a = merged[-1] + b = pts[i] + c = pts[i + 1] + colinear = (abs(a[0] - b[0]) < 1.2 and abs(b[0] - c[0]) < 1.2) or ( + abs(a[1] - b[1]) < 1.2 and abs(b[1] - c[1]) < 1.2 + ) + if colinear: + continue + merged.append(b) + merged.append(pts[-1]) + return _dedupe_pts(merged) + + +def _polyline_backtracks(pts: list[tuple[float, float]]) -> bool: + pts = _simplify_ortho(pts) + for a, b, c in zip(pts, pts[1:], pts[2:]): + if abs(a[0] - b[0]) < 1.2 and abs(b[0] - c[0]) < 1.2: + if (b[1] - a[1]) * (c[1] - b[1]) < -1.0: + return True + if abs(a[1] - b[1]) < 1.2 and abs(b[1] - c[1]) < 1.2: + if (b[0] - a[0]) * (c[0] - b[0]) < -1.0: + return True + return False + + +def _seg_aabb(a: tuple[float, float], b: tuple[float, float], r: _Rect) -> bool: + dx, dy = b[0] - a[0], b[1] - a[1] + t0, t1 = 0.0, 1.0 + for p, q in ( + (-dx, a[0] - r.x), + (dx, r.r - a[0]), + (-dy, a[1] - r.y), + (dy, r.b - a[1]), + ): + if abs(p) < 1e-9: + if q < 0: + return False + continue + t = q / p + if p < 0: + if t > t1: + return False + if t > t0: + t0 = t + else: + if t < t0: + return False + if t < t1: + t1 = t + return t1 >= t0 + + +def _seg_hits_open_box(a: tuple[float, float], b: tuple[float, float], r: _Rect, inset: float = 4.0) -> bool: + inner = r.inflate(-inset) + if inner.w <= 2 or inner.h <= 2: + return False + return _seg_aabb(a, b, inner) + + +def _seg_obstacle(a: tuple[float, float], b: tuple[float, float], half: float) -> _Rect: + x0, x1 = min(a[0], b[0]) - half, max(a[0], b[0]) + half + y0, y1 = min(a[1], b[1]) - half, max(a[1], b[1]) + half + return _Rect(x0, y0, x1 - x0, y1 - y0) + + +def _take_free(used: list[float], hint: float, lo: float, hi: float, gap: float) -> float: + if hi < lo: + lo, hi = hi, lo + if hi - lo < gap * 0.4: + used.append((lo + hi) / 2) + return used[-1] + hint = min(max(hint, lo), hi) + for step in range(0, 56): + delta = (step + 1) // 2 * gap * 0.5 + x = hint + (delta if step % 2 == 0 else -delta) + x = min(max(x, lo), hi) + if all(abs(x - u) >= gap for u in used): + used.append(x) + return x + used.append(hint) + return hint + + +def _route(p1: tuple[float, float], d1: str, p2: tuple[float, float], d2: str, stub: float) -> list[tuple[float, float]]: + """Elbow helper; ICOM routing uses _route_icom instead.""" + a = _offset(p1, d1, stub) + b = _offset(p2, d2, stub) + pts = [p1, a] + if abs(a[0] - b[0]) < 1.5 or abs(a[1] - b[1]) < 1.5: + pts.append(b) + elif d1 in ("E", "W"): + pts.extend([(b[0], a[1]), b]) + else: + pts.extend([(a[0], b[1]), b]) + pts.append(p2) + return _simplify_ortho(pts) + + +def _draw_tunnel(scene: _Scene, pt: tuple[float, float], horizontal: bool, s: float, lw: float) -> None: + x, y = pt + r = 7 * s + if horizontal: + scene.arc((x - r, y - r, x + r * 0.15, y + r), 70, 290, lw) + scene.arc((x - r * 0.15, y - r, x + r, y + r), 250, 110, lw) + else: + scene.arc((x - r, y - r, x + r, y + r * 0.15), 160, 20, lw) + scene.arc((x - r, y - r * 0.15, x + r, y + r), 340, 200, lw) + + +def _draw_context_cell( + scene: _Scene, + d: Idef0Diagram, + cell: _Rect, + s: float, + fonts: dict, + lw: float, +) -> None: + """CONTEXT thumbnail: small TOP, or a mini parent box / staircase.""" + scene.text(cell.x + 6 * s, cell.y + 4 * s, "CONTEXT:", fonts["lab"], anchor="lt") + ctx = d.context or ("TOP" if (d.node or "").upper() == "A-0" else d.node) + if (ctx or "").upper() == "TOP": + scene.text(cell.cx, cell.cy + 8 * s, "TOP", 12 * s, bold=True, anchor="mm") + return + child: int | None = None + m = re.match(r"^A(\d+)$", (d.node or "").strip(), re.I) + if m and int(m.group(1)) >= 1: + child = int(m.group(1)) + # A-0 parent is a single box; A0+ child diagrams get a mini staircase. + if child is None or (ctx or "").upper() in {"A-0"}: + bw, bh = min(58 * s, cell.w * 0.46), min(28 * s, cell.h * 0.32) + bx, by = cell.cx - bw / 2, cell.y + 32 * s + sh = 2.0 * s + scene.rect(bx + sh, by + sh, bw, bh, width=0, fill="#c4c4c4", outline=None) + scene.rect(bx, by, bw, bh, width=lw, fill="#ffffff") + scene.text(bx + bw - 4 * s, by + bh - 3 * s, ctx, fonts["small"], bold=True, anchor="rb") + return + n = 4 + bw, bh = 15 * s, 9 * s + step_x, step_y = 8 * s, 7 * s + total_w = bw + step_x * (n - 1) + total_h = bh + step_y * (n - 1) + x0 = cell.cx - total_w / 2 + y0 = cell.y + 24 * s + fill_i = min(max(child, 1), n) - 1 + for i in range(n): + x, y = x0 + i * step_x, y0 + i * step_y + scene.rect(x, y, bw, bh, width=max(0.8, lw * 0.85), fill="#000000" if i == fill_i else "#ffffff") + scene.text(cell.x + 6 * s, min(cell.b - 5 * s, y0 + total_h + 4 * s), ctx, fonts["small"], bold=True, anchor="lt") + + +def _draw_kit_form(scene: _Scene, d: Idef0Diagram, outer: _Rect, s: float, fonts: dict) -> _Rect: + """NIST / KBSI IDEF0 sheet. Returns the inner working rectangle.""" + lw = max(1.0, 1.0 * s) + # double outer frame + scene.rect(outer.x, outer.y, outer.w, outer.h, width=lw) + inset = 3.2 * s + scene.rect(outer.x + inset, outer.y + inset, outer.w - 2 * inset, outer.h - 2 * inset, width=lw) + + inner = _Rect(outer.x + inset, outer.y + inset, outer.w - 2 * inset, outer.h - 2 * inset) + header_h = 96 * s + footer_h = 64 * s + hy = inner.y + fy = inner.b - footer_h + # header / footer separators + scene.line(inner.x, hy + header_h, inner.r, hy + header_h, lw) + scene.line(inner.x, fy, inner.r, fy, lw) + + # column x positions inside inner + x0 = inner.x + w = inner.w + x_used = x0 + w_used = 0.145 * w + x_auth = x_used + w_used + w_auth = 0.355 * w + x_stat = x_auth + w_auth + w_stat = 0.175 * w + x_read = x_stat + w_stat + w_read = 0.145 * w + x_ctx = x_read + w_read + w_ctx = inner.r - x_ctx + + for x in (x_auth, x_stat, x_read, x_ctx): + scene.line(x, hy, x, hy + header_h, lw) + + # DATE/REV row inside status column + date_row = 22 * s + scene.line(x_stat, hy + date_row, x_read, hy + date_row, lw) + scene.line(x_stat + w_stat * 0.55, hy, x_stat + w_stat * 0.55, hy + date_row, lw) + + lab = fonts["lab"] + val = fonts["val"] + small = fonts["small"] + + scene.text(x_used + 6 * s, hy + 5 * s, "USED AT:", lab, anchor="lt") + if d.used_at: + for i, ln in enumerate(_wrap(d.used_at, _load_font(int(val), False), int(w_used - 12 * s))[:4]): + scene.text(x_used + 6 * s, hy + 22 * s + i * 14 * s, ln, val, anchor="lt") + + scene.text(x_auth + 6 * s, hy + 4 * s, "AUTHOR:", lab, anchor="lt") + scene.text(x_auth + 62 * s, hy + 4 * s, d.author, val, bold=False, anchor="lt") + scene.text(x_auth + 6 * s, hy + 22 * s, "PROJECT:", lab, anchor="lt") + proj_lines = _wrap(d.project, _load_font(int(small), False), int(w_auth - 78 * s)) + if proj_lines: + scene.text(x_auth + 70 * s, hy + 22 * s, proj_lines[0], small, anchor="lt") + for i, ln in enumerate(proj_lines[1:3], 1): + scene.text(x_auth + 70 * s, hy + 22 * s + i * 13 * s, ln, small, anchor="lt") + notes_y = hy + 54 * s + scene.text(x_auth + 6 * s, notes_y, "NOTES:", lab, anchor="lt") + marked = set() + for tok in re.findall(r"\d+", d.notes or ""): + marked.add(int(tok)) + nx = x_auth + 58 * s + for n in range(1, 11): + scene.text(nx, notes_y, str(n), small, bold=n in marked, anchor="lt") + nx += 14 * s + + scene.text(x_stat + 5 * s, hy + 4 * s, "DATE:", lab, anchor="lt") + scene.text(x_stat + 42 * s, hy + 4 * s, d.date, val, anchor="lt") + scene.text(x_stat + w_stat * 0.55 + 4 * s, hy + 4 * s, "REV:", lab, anchor="lt") + scene.text(x_stat + w_stat * 0.55 + 36 * s, hy + 4 * s, d.rev, val, anchor="lt") + + st_y = hy + date_row + 6 * s + box_s = 8 * s + for i, name in enumerate(_STATUSES): + yy = st_y + i * 16 * s + filled = name == d.status + scene.rect( + x_stat + 6 * s, + yy + 1 * s, + box_s, + box_s, + width=lw, + fill="#000000" if filled else None, + ) + scene.text(x_stat + 18 * s, yy, name, small, bold=filled, anchor="lt") + + scene.text(x_read + 6 * s, hy + 4 * s, "READER", lab, anchor="lt") + scene.text(x_read + 6 * s, hy + 22 * s, d.reader, val, anchor="lt") + scene.line(x_read, hy + date_row, x_ctx, hy + date_row, lw) + scene.text(x_read + 6 * s, hy + date_row + 4 * s, "DATE", lab, anchor="lt") + scene.text(x_read + 6 * s, hy + date_row + 20 * s, d.reader_date, val, anchor="lt") + + _draw_context_cell(scene, d, _Rect(x_ctx, hy, w_ctx, header_h), s, fonts, lw) + + # footer + fn_w = 0.18 * inner.w + ft_x = inner.x + fn_w + ft_w = 0.62 * inner.w + fnum_x = ft_x + ft_w + scene.line(ft_x, fy, ft_x, inner.b, lw) + scene.line(fnum_x, fy, fnum_x, inner.b, lw) + scene.text(inner.x + 6 * s, fy + 4 * s, "NODE:", lab, anchor="lt") + scene.text(inner.x + fn_w / 2, fy + 38 * s, d.node, fonts["node"], bold=True, anchor="mm") + scene.text(ft_x + 6 * s, fy + 4 * s, "TITLE:", lab, anchor="lt") + title_font = _load_font(int(fonts["title"]), True) + tlines = _wrap(d.title, title_font, int(ft_w - 16 * s)) + if len(tlines) == 1: + scene.text(ft_x + ft_w / 2, fy + 38 * s, tlines[0], fonts["title"], bold=True, anchor="mm") + else: + for i, ln in enumerate(tlines[:2]): + scene.text(ft_x + ft_w / 2, fy + 26 * s + i * 18 * s, ln, fonts["title"] * 0.85, bold=True, anchor="mm") + scene.text(fnum_x + 6 * s, fy + 4 * s, "NUMBER:", lab, anchor="lt") + scene.text(fnum_x + (inner.r - fnum_x) / 2, fy + 36 * s, d.number, fonts["node"], bold=True, anchor="mm") + + # tiny page box, bottom-right of the form + pb = 18 * s + scene.rect(inner.r - pb, inner.b - pb, pb, pb, width=lw) + scene.text(inner.r - pb / 2, inner.b - pb / 2, d.page, small, bold=True, anchor="mm") + + work = _Rect(inner.x + 8 * s, hy + header_h + 8 * s, inner.w - 16 * s, fy - (hy + header_h) - 16 * s) + return work + + +def _assign_slots(diagram: Idef0Diagram, geoms: dict[str, _BoxGeom]) -> None: + for g in geoms.values(): + g.slots = {"I": [], "C": [], "O": [], "M": [], "CALL": []} + for ai, arrow in enumerate(diagram.arrows): + for end in arrow.sources + arrow.targets: + if end.box and end.box in geoms: + side = end.side if end.side in geoms[end.box].slots else "M" + geoms[end.box].slots[side].append((ai, arrow, end)) + + +def _slot_index(geom: _BoxGeom, arrow: Idef0Arrow, end: Idef0End) -> tuple[int, int]: + side = end.side if end.side in geom.slots else "M" + items = geom.slots[side] + for i, (_ai, ar, e) in enumerate(items): + if ar is arrow and e is end: + return i, len(items) + return 0, max(len(items), 1) + + +def _layout_boxes(diagram: Idef0Diagram, work: _Rect, s: float) -> dict[str, _BoxGeom]: + """BPwin-style cluster: compact staircase, large ICOM margins, do not stretch to fill.""" + n = len(diagram.boxes) + geoms: dict[str, _BoxGeom] = {} + c_h, m_h = 84 * s, 96 * s + i_w, o_w = 108 * s, 100 * s + inner = _Rect( + work.x + i_w, + work.y + c_h, + max(180 * s, work.w - i_w - o_w), + max(140 * s, work.h - c_h - m_h), + ) + if n == 1: + bw, bh = min(320 * s, inner.w * 0.42), min(138 * s, inner.h * 0.36) + rect = _Rect(inner.cx - bw / 2, inner.cy - bh / 2, bw, bh) + geoms[diagram.boxes[0].id] = _BoxGeom(diagram.boxes[0], rect) + return geoms + bw = min(200 * s, inner.w * 0.34) + bh = min(86 * s, inner.h * 0.24) + # Compact staircase, but keep a corridor so O→I can drop between boxes + # (0.85*bw alone overlaps; Kinzyabulatov A1 has a visible gap). + step_x = bw + 56 * s + step_y = bh + 58 * s + total_w = bw + step_x * (n - 1) + total_h = bh + step_y * (n - 1) + if n > 1 and total_w > inner.w: + step_x = max(bw * 0.55, (inner.w - bw) / (n - 1)) + total_w = bw + step_x * (n - 1) + if n > 1 and total_h > inner.h: + step_y = max(bh * 0.7, (inner.h - bh) / (n - 1)) + total_h = bh + step_y * (n - 1) + x0 = inner.x + max(0.0, (inner.w - total_w) / 2) + y0 = inner.y + max(0.0, (inner.h - total_h) / 2) + for i, box in enumerate(diagram.boxes): + geoms[box.id] = _BoxGeom(box, _Rect(x0 + i * step_x, y0 + i * step_y, bw, bh)) + return geoms + + +def _draw_box(scene: _Scene, geom: _BoxGeom, s: float, lw: float, fonts: dict) -> None: + r = geom.rect + sh = 4.0 * s + scene.rect(r.x + sh, r.y + sh, r.w, r.h, width=0, fill="#c0c0c0", outline=None) + scene.rect(r.x, r.y, r.w, r.h, width=max(1.0, lw), fill="#ffffff") + # BPwin "has child" tick in the top-left corner. + ts = 8 * s + scene.polygon( + [(r.x, r.y), (r.x + ts, r.y), (r.x, r.y + ts)], + fill="#000000", + outline="#000000", + width=1, + ) + name_font = _load_font(int(fonts["box"]), False) + lines = _wrap(geom.box.name, name_font, int(r.w - 18 * s)) + total_h = len(lines) * fonts["box"] * 1.15 + y0 = r.cy - total_h / 2 + fonts["box"] * 0.35 + for i, ln in enumerate(lines[:4]): + scene.text(r.cx, y0 + i * fonts["box"] * 1.15, ln, fonts["box"], anchor="mm") + scene.text(r.r - 6 * s, r.b - 5 * s, geom.box.number or geom.box.id, fonts["id"], anchor="rb") + + +def _end_point( + end: Idef0End, + arrow: Idef0Arrow, + geoms: dict[str, _BoxGeom], + work: _Rect, + mate: Idef0End | None = None, +) -> tuple[float, float]: + if end.box and end.box in geoms: + g = geoms[end.box] + idx, cnt = _slot_index(g, arrow, end) + return _port_point(g, end.side, idx, cnt) + other_y = work.cy + other_x = work.cx + mates = [mate] if mate is not None else ( + arrow.targets if end in arrow.sources else arrow.sources + ) + xs, ys = [], [] + for m in mates: + if m is None or not m.box or m.box not in geoms: + continue + g = geoms[m.box] + idx, cnt = _slot_index(g, arrow, m) + px, py = _port_point(g, m.side, idx, cnt) + xs.append(px) + ys.append(py) + if xs: + other_x = sum(xs) / len(xs) + other_y = sum(ys) / len(ys) + return _boundary_point(work, end.side, other_y if end.side in ("I", "O") else other_x) + + +def _box_order(diagram: Idef0Diagram) -> dict[str, int]: + return {b.id: i for i, b in enumerate(diagram.boxes)} + + +def _is_feedback( + src: Idef0End, + tgt: Idef0End, + order: dict[str, int], +) -> bool: + if not src.box or not tgt.box: + return False + if src.box not in order or tgt.box not in order: + return False + return order[src.box] > order[tgt.box] and src.side == "O" and tgt.side == "I" + + +@dataclass +class _Channels: + work: _Rect + stub: float + min_gap: float + c_lanes: dict[int, float] + m_lanes: dict[int, float] + i_x: float + o_x: float + m_y: float + box_top: float + box_bot: float + used_v: list[float] = field(default_factory=list) + used_h: list[float] = field(default_factory=list) + + +def _make_channels( + diagram: Idef0Diagram, + geoms: dict[str, _BoxGeom], + work: _Rect, + s: float, +) -> _Channels: + box_top = min((g.rect.y for g in geoms.values()), default=work.y + 70 * s) + box_bot = max((g.rect.b for g in geoms.values()), default=work.b - 70 * s) + c_arrows = [ + a for a in diagram.arrows + if any(e.box is None and e.side == "C" for e in a.sources) + ] + m_arrows = [ + a for a in diagram.arrows + if any(e.box is None and e.side == "M" for e in a.sources) + ] + c_arrows.sort(key=lambda a: (-len(a.targets), a.icom or a.label)) + m_arrows.sort(key=lambda a: (-len(a.targets), a.icom or a.label)) + c_lanes: dict[int, float] = {} + for k, a in enumerate(c_arrows): + y = work.y + 24 * s + k * 32 * s + c_lanes[id(a)] = min(y, box_top - 22 * s) + m_lanes: dict[int, float] = {} + for k, a in enumerate(m_arrows): + y = box_bot + 24 * s + k * 32 * s + m_lanes[id(a)] = min(y, work.b - 18 * s) + used_h = list(c_lanes.values()) + list(m_lanes.values()) + return _Channels( + work=work, + stub=28 * s, + min_gap=32 * s, + c_lanes=c_lanes, + m_lanes=m_lanes, + i_x=work.x + 18 * s, + o_x=work.r - 18 * s, + m_y=min(work.b - 16 * s, max(box_bot + 56 * s, work.b - 28 * s)), + box_top=box_top, + box_bot=box_bot, + used_v=[], + used_h=used_h, + ) + + +def _gutter_x( + p1: tuple[float, float], + p2: tuple[float, float], + geoms: dict[str, _BoxGeom], + skip: set[str], + stub: float, +) -> float: + lo, hi = (p1[0], p2[0]) if p1[0] <= p2[0] else (p2[0], p1[0]) + candidates = [ + (p1[0] + p2[0]) / 2, + p1[0] + stub, + p2[0] - stub, + lo + stub, + hi - stub, + ] + y1, y2 = p1[1], p2[1] + for gx in candidates: + if gx < lo - 1 or gx > hi + 1: + continue + ok = True + for gid, g in geoms.items(): + if gid in skip: + continue + if _seg_hits_open_box((gx, y1), (gx, y2), g.rect) or _seg_hits_open_box( + (p1[0], y1), (gx, y1), g.rect + ): + ok = False + break + if ok: + return gx + return max(lo + stub, min(hi - stub, (p1[0] + p2[0]) / 2)) + + +def _route_icom( + src: Idef0End, + tgt: Idef0End, + p1: tuple[float, float], + p2: tuple[float, float], + ch: _Channels, + geoms: dict[str, _BoxGeom], + order: dict[str, int], + arrow: Idef0Arrow, + lane_y: float | None = None, + spine_x: float | None = None, +) -> list[tuple[float, float]]: + stub = ch.stub + skip = {b for b in (src.box, tgt.box) if b} + + if src.box is None and tgt.box and src.side == "C": + if spine_x is not None and lane_y is not None: + y = lane_y + x_drop = p2[0] + x_in = spine_x + return _simplify_ortho([ + (x_in, ch.work.y), + (x_in, y), + (x_drop, y), + (x_drop, p2[1]), + ]) + return _simplify_ortho([(p2[0], ch.work.y), p2]) + + if src.box is None and tgt.box and src.side == "I": + return _simplify_ortho([(ch.work.x, p2[1]), p2]) + + if src.box is None and tgt.box and src.side == "M": + if spine_x is not None and lane_y is not None: + y = lane_y + x_up = p2[0] + x_in = spine_x + return _simplify_ortho([ + (x_in, ch.work.b), + (x_in, y), + (x_up, y), + (x_up, p2[1]), + ]) + return _simplify_ortho([(p2[0], ch.work.b), p2]) + + if tgt.box is None and src.box and tgt.side == "O": + return _simplify_ortho([p1, (ch.work.r, p1[1])]) + + if tgt.box is None and src.box and tgt.side == "CALL": + return _simplify_ortho([p1, (p1[0], ch.work.b)]) + + if src.box and tgt.box and src.side == "O" and tgt.side == "I": + if _is_feedback(src, tgt, order): + right = _take_free(ch.used_v, max(p1[0] + stub, ch.o_x), ch.o_x - 8, ch.work.r - 8, ch.min_gap) + bottom = _take_free(ch.used_h, ch.m_y, ch.box_bot + 12, ch.work.b - 10, ch.min_gap) + left = _take_free(ch.used_v, ch.i_x, ch.work.x + 8, ch.i_x + 40, ch.min_gap) + return _simplify_ortho([ + p1, + (right, p1[1]), + (right, bottom), + (left, bottom), + (left, p2[1]), + p2, + ]) + src_r = geoms[src.box].rect + tgt_r = geoms[tgt.box].rect + if tgt_r.x >= src_r.r + 10: + lo, hi = p1[0], p2[0] + hint = _gutter_x(p1, p2, geoms, skip, stub) + gx = _take_free(ch.used_v, hint, lo + stub * 0.45, hi - stub * 0.35, ch.min_gap * 0.6) + return _simplify_ortho([p1, (gx, p1[1]), (gx, p2[1]), p2]) + # Overlapping staircase: out, down the notch, in from the left of the target. + gx = _take_free( + ch.used_v, src_r.r + stub, src_r.r + stub * 0.55, src_r.r + stub * 2.8, ch.min_gap, + ) + left = _take_free( + ch.used_v, tgt_r.x - stub, ch.work.x + 8, max(ch.work.x + 10, tgt_r.x - stub * 0.45), ch.min_gap, + ) + y_lo = min(src_r.b + 8, tgt_r.y - 8) + y_hi = max(src_r.b + 8, tgt_r.y - 8) + y_bar = _take_free(ch.used_h, (src_r.b + tgt_r.y) / 2, y_lo, y_hi, ch.min_gap) + return _simplify_ortho([p1, (gx, p1[1]), (gx, y_bar), (left, y_bar), (left, p2[1]), p2]) + + if src.box and tgt.box and src.side == "O" and tgt.side == "C": + clear_x = _take_free( + ch.used_v, p1[0] + stub, p1[0] + stub * 0.7, p1[0] + stub * 3.2, ch.min_gap, + ) + y_bar = min(p2[1] - stub, geoms[tgt.box].rect.y - stub) + y_bar = max(ch.work.y + 12, y_bar) + y_bar = _take_free(ch.used_h, y_bar, ch.work.y + 10, geoms[tgt.box].rect.y - 12, ch.min_gap) + return _simplify_ortho([ + p1, + (clear_x, p1[1]), + (clear_x, y_bar), + (p2[0], y_bar), + p2, + ]) + + if src.box and tgt.box and src.side == "O" and tgt.side == "M": + clear_x = p1[0] + stub + y_bar = max(p2[1] + stub, geoms[tgt.box].rect.b + stub) + return _simplify_ortho([ + p1, + (clear_x, p1[1]), + (clear_x, y_bar), + (p2[0], y_bar), + p2, + ]) + + return _simplify_ortho([p1, p2]) + + +def _poly_len(pts: list[tuple[float, float]]) -> float: + tot = 0.0 + for a, b in zip(pts, pts[1:]): + tot += math.hypot(b[0] - a[0], b[1] - a[1]) + return tot + + +def _shared_prefix(paths: list[list[tuple[float, float]]]) -> list[tuple[float, float]]: + if not paths: + return [] + i = 0 + while all(i < len(p) for p in paths): + x0, y0 = paths[0][i] + if any(abs(p[i][0] - x0) > 1.5 or abs(p[i][1] - y0) > 1.5 for p in paths): + break + i += 1 + return paths[0][:i] + + +def _shared_suffix(paths: list[list[tuple[float, float]]]) -> list[tuple[float, float]]: + if not paths: + return [] + i = 0 + while all(i < len(p) for p in paths): + x0, y0 = paths[0][-1 - i] + if any(abs(p[-1 - i][0] - x0) > 1.5 or abs(p[-1 - i][1] - y0) > 1.5 for p in paths): + break + i += 1 + if i <= 0: + return [] + return paths[0][-i:] + + +def _best_label_polyline(paths: list[list[tuple[float, float]]], min_len: float) -> list[tuple[float, float]]: + """Prefer the fork/join spine; if it has a horizontal bar, label that.""" + if not paths: + return [] + if len(paths) == 1: + return paths[0] + prefix = _shared_prefix(paths) + if len(prefix) >= 2: + y = prefix[-1][1] + xs = [pt[0] for path in paths for pt in path if abs(pt[1] - y) < 2.5] + if xs and max(xs) - min(xs) >= min_len: + return _simplify_ortho(prefix + [(min(xs), y), (max(xs), y)]) + suffix = _shared_suffix(paths) + plen, slen = _poly_len(prefix), _poly_len(suffix) + if plen >= min_len and plen >= slen: + return prefix + if slen >= min_len: + return suffix + return max(paths, key=_poly_len) + + +def _segments( + pts: list[tuple[float, float]], +) -> list[tuple[tuple[float, float], tuple[float, float], str, float]]: + segs = [] + for a, b in zip(pts, pts[1:]): + dx, dy = b[0] - a[0], b[1] - a[1] + length = math.hypot(dx, dy) + orient = "H" if abs(dx) >= abs(dy) else "V" + segs.append((a, b, orient, length)) + return segs + + +def _pick_label_segment( + pts: list[tuple[float, float]], + min_len: float, +) -> tuple[tuple[float, float], tuple[float, float], str] | None: + segs = _segments(pts) + if not segs: + return None + + def score(sg: tuple) -> float: + _a, _b, orient, length = sg + sc = 40.0 if orient == "H" else 0.0 + sc -= abs(length - 110.0) * 0.12 + if length < min_len: + sc -= 50 + if length > 420: + sc -= 90 + return sc + + chosen = max(segs, key=score) + return chosen[0], chosen[1], chosen[2] + + +def _seg_attach( + a: tuple[float, float], + b: tuple[float, float], + t: float = 0.48, +) -> tuple[float, float]: + return a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t + + +def _closest_on_rect(r: _Rect, px: float, py: float) -> tuple[float, float]: + if r.contains_point(px, py): + dists = [ + (px - r.x, (r.x, py)), + (r.r - px, (r.r, py)), + (py - r.y, (px, r.y)), + (r.b - py, (px, r.b)), + ] + return min(dists, key=lambda t: t[0])[1] + return min(max(px, r.x), r.r), min(max(py, r.y), r.b) + + +def _clamp_rect(r: _Rect, work: _Rect) -> _Rect: + w = min(r.w, work.w) + h = min(r.h, work.h) + x = min(max(r.x, work.x), work.r - w) + y = min(max(r.y, work.y), work.b - h) + return _Rect(x, y, w, h) + + +def _label_block_size( + lines: list[str], + font: ImageFont.ImageFont, + size: float, + pad: float, +) -> tuple[float, float]: + widths = [_text_wh(font, ln)[0] for ln in lines] or [0] + line_h = size * 1.12 + return max(widths) + 2 * pad, line_h * len(lines) + 2 * pad + + +def _layout_arrow_label( + pts: list[tuple[float, float]], + label: str, + font: ImageFont.ImageFont, + size: float, + s: float, + work: _Rect, + obstacles: list[_Rect], + boxes: list[_Rect] | None = None, + shafts: list[_Rect] | None = None, +) -> tuple[_Rect, tuple[float, float], tuple[float, float], list[str], str] | None: + """Place a noun-phrase off the shaft (FIPS 183). Returns halo, attach, connect, lines, orient.""" + label = (label or "").strip() + if not label or len(pts) < 2: + return None + boxes = boxes or [] + shafts = shafts or [] + max_w = int(92 * s) + lines = _wrap(label, font, max_w)[:3] + pad = 2.8 * s + tw, th = _label_block_size(lines, font, size, pad) + picked = _pick_label_segment(pts, min_len=22 * s) + if picked is None: + return None + a, b, orient = picked + gap = 16 * s + best = None + best_score = -1e18 + best_attach = _seg_attach(a, b) + ts = (0.78, 0.68, 0.58, 0.48, 0.38, 0.28, 0.18, 0.88) + for t in ts: + attach = _seg_attach(a, b, t) + ax, ay = attach + if orient == "H": + raw = [ + ("above", ax - tw / 2, ay - gap - th), + ("below", ax - tw / 2, ay + gap), + ("aboveL", ax - tw, ay - gap - th), + ("aboveR", ax, ay - gap - th), + ("left", ax - gap - tw, ay - th / 2), + ("right", ax + gap, ay - th / 2), + ] + else: + prefer_left = ax > work.cx + side_order = ( + [("left", ax - gap - tw, ay - th / 2), ("right", ax + gap, ay - th / 2)] + if prefer_left + else [("right", ax + gap, ay - th / 2), ("left", ax - gap - tw, ay - th / 2)] + ) + raw = side_order + [ + ("above", ax - tw / 2, ay - gap - th), + ("below", ax - tw / 2, ay + gap), + ] + for i, (_side, x, y) in enumerate(raw): + halo = _clamp_rect(_Rect(x, y, tw, th), work) + score = 70.0 - i * 14 - abs(t - 0.5) * 12 + if halo.contains_point(ax, ay, pad=3 * s): + score -= 600 + box_hit = sum(halo.overlap_area(box) for box in boxes) + if box_hit > 1: + score -= 1e5 + box_hit + shaft_hit = sum(halo.overlap_area(sh) for sh in shafts) + if shaft_hit > (6 * s) * (6 * s): + score -= 2500 + shaft_hit / max(s * s, 1.0) * 4 + for obs in obstacles: + ov = halo.overlap_area(obs) + if ov > 0: + score -= ov / max(s * s, 1.0) * 10 + if halo.x < work.x + 2 or halo.r > work.r - 2: + score -= 25 + if halo.y < work.y + 2 or halo.b > work.b - 2: + score -= 25 + if score > best_score: + best_score = score + best = halo + best_attach = attach + if best is None or best_score < -9e4: + return None + connect = _closest_on_rect(best, best_attach[0], best_attach[1]) + return best, best_attach, connect, lines, orient + + +def _draw_squiggle( + scene: _Scene, + p0: tuple[float, float], + p1: tuple[float, float], + s: float, + lw: float, +) -> None: + """FIPS 183 squiggle: a short tapered sine, not a lightning bolt.""" + x0, y0 = p0 + x1, y1 = p1 + dx, dy = x1 - x0, y1 - y0 + dist = math.hypot(dx, dy) + if dist < 4 * s: + return + maxd = 15 * s + if dist > maxd: + ux, uy = dx / dist, dy / dist + x1, y1 = x0 + ux * maxd, y0 + uy * maxd + dx, dy, dist = x1 - x0, y1 - y0, maxd + ux, uy = dx / dist, dy / dist + nx, ny = -uy, ux + amp = 2.3 * s + n = 16 + pts = [] + for i in range(n + 1): + t = i / n + wave = math.sin(t * math.pi * 4.0) * math.sin(t * math.pi) + pts.append((x0 + ux * dist * t + nx * amp * wave, y0 + uy * dist * t + ny * amp * wave)) + scene.polyline(pts, width=max(lw * 0.8, 1.0)) + + +def _draw_label_block(scene: _Scene, halo: _Rect, lines: list[str], size: float) -> None: + scene.rect(halo.x, halo.y, halo.w, halo.h, width=0, fill="#ffffff", outline=None) + line_h = size * 1.12 + total = line_h * len(lines) + y0 = halo.cy - total / 2 + size * 0.36 + for i, ln in enumerate(lines): + scene.text(halo.cx, y0 + i * line_h, ln, size, anchor="mm") + + +def _draw_arrow_label( + scene: _Scene, + pts: list[tuple[float, float]], + label: str, + s: float, + lw: float, + fonts: dict, + work: _Rect, + occupied: list[_Rect], + boxes: list[_Rect], + shafts: list[_Rect] | None = None, +) -> _Rect | None: + font = _load_font(int(fonts["arrow"]), False) + placed = _layout_arrow_label( + pts, label, font, fonts["arrow"], s, work, occupied, boxes, shafts, + ) + if placed is None: + return None + halo, attach, connect, lines, _orient = placed + dist = math.hypot(connect[0] - attach[0], connect[1] - attach[1]) + if dist > 18 * s: + _draw_squiggle(scene, connect, attach, s, lw) + _draw_label_block(scene, halo, lines, fonts["arrow"]) + occupied.append(halo.inflate(6 * s)) + return halo + + +def _icom_border_point( + arrow: Idef0Arrow, + geoms: dict[str, _BoxGeom], + work: _Rect, +) -> tuple[Idef0End, tuple[float, float]] | None: + """Where the ICOM arrow meets the inner work frame.""" + bound = next((e for e in arrow.sources + arrow.targets if e.box is None), None) + if bound is None: + return None + mates = arrow.targets if bound in arrow.sources else arrow.sources + xs, ys = [], [] + for m in mates: + if not m.box or m.box not in geoms: + continue + g = geoms[m.box] + idx, cnt = _slot_index(g, arrow, m) + px, py = _port_point(g, m.side, idx, cnt) + xs.append(px) + ys.append(py) + if bound.side == "C": + x = (min(xs) + max(xs)) / 2 if xs else work.cx + return bound, (x, work.y) + if bound.side == "M": + x = (min(xs) + max(xs)) / 2 if xs else work.cx + return bound, (x, work.b) + if bound.side == "I": + y = (min(ys) + max(ys)) / 2 if ys else work.cy + return bound, (work.x, y) + if bound.side in ("O", "CALL"): + y = (min(ys) + max(ys)) / 2 if ys else work.cy + return bound, (work.r if bound.side == "O" else work.cx, work.b if bound.side == "CALL" else y) + return bound, _end_point(bound, arrow, geoms, work) + + +def _draw_icom_code( + scene: _Scene, + arrow: Idef0Arrow, + geoms: dict[str, _BoxGeom], + work: _Rect, + s: float, + fonts: dict, + occupied: list[_Rect], +) -> None: + if not arrow.icom: + return + hit = _icom_border_point(arrow, geoms, work) + if hit is None: + return + bound, pt = hit + font = _load_font(int(fonts["id"]), True) + tw, th = _text_wh(font, arrow.icom) + pad = 1.6 * s + bx, by = pt + if bound.side == "I": + halo = _Rect(work.x + 2 * s, by - th - 5 * s, tw + 2 * pad, th + 2 * pad) + elif bound.side == "O": + halo = _Rect(work.r - tw - 2 * pad - 2 * s, by - th - 5 * s, tw + 2 * pad, th + 2 * pad) + elif bound.side == "C": + halo = _Rect(bx + 5 * s, work.y + 2 * s, tw + 2 * pad, th + 2 * pad) + else: + halo = _Rect(bx + 5 * s, work.b - th - 2 * pad - 2 * s, tw + 2 * pad, th + 2 * pad) + halo = _clamp_rect(halo, work) + scene.rect(halo.x, halo.y, halo.w, halo.h, width=0, fill="#ffffff", outline=None) + scene.text(halo.cx, halo.cy, arrow.icom, fonts["id"], bold=True, anchor="mm") + occupied.append(halo.inflate(3 * s)) + + +def _append_tunnel( + tunnels: list[tuple[tuple[float, float], bool]], + end: Idef0End, + pt: tuple[float, float], + s: float, +) -> None: + if end.tunnel: + tunnels.append((_offset(pt, _end_stub_dir(end), 10 * s), end.side in ("I", "O"))) + + +def _seg_key(a: tuple[float, float], b: tuple[float, float]) -> tuple: + pa = (round(a[0], 1), round(a[1], 1)) + pb = (round(b[0], 1), round(b[1], 1)) + return (pa, pb) if pa <= pb else (pb, pa) + + +def _draw_arrow( + scene: _Scene, + arrow: Idef0Arrow, + geoms: dict[str, _BoxGeom], + work: _Rect, + s: float, + lw: float, + order: dict[str, int], + ch: _Channels, +) -> list[list[tuple[float, float]]]: + srcs = arrow.sources + tgts = arrow.targets + paths: list[list[tuple[float, float]]] = [] + heads: list[tuple[tuple[float, float], str]] = [] + tunnels: list[tuple[tuple[float, float], bool]] = [] + + if len(srcs) == 1 and len(tgts) >= 1: + tgt_pts = [_end_point(t, arrow, geoms, work) for t in tgts] + p1 = _end_point(srcs[0], arrow, geoms, work, mate=tgts[0] if len(tgts) == 1 else None) + spine_x = None + lane_y = None + if srcs[0].box is None and srcs[0].side == "C" and len(tgts) > 1: + xs = [p[0] for p in tgt_pts] + spine_x = (min(xs) + max(xs)) / 2 + lane_y = ch.c_lanes.get(id(arrow), work.y + 18 * s) + p1 = (spine_x, work.y) + for x in xs: + if all(abs(x - u) >= ch.min_gap * 0.45 for u in ch.used_v): + ch.used_v.append(x) + elif srcs[0].box is None and srcs[0].side == "M" and len(tgts) > 1: + xs = [p[0] for p in tgt_pts] + spine_x = (min(xs) + max(xs)) / 2 + lane_y = ch.m_lanes.get(id(arrow), work.b - 18 * s) + p1 = (spine_x, work.b) + for x in xs: + if all(abs(x - u) >= ch.min_gap * 0.45 for u in ch.used_v): + ch.used_v.append(x) + mixed_oc = ( + srcs[0].box + and srcs[0].side == "O" + and any(t.box is None and t.side == "O" for t in tgts) + and any(t.box and t.side == "C" for t in tgts) + ) + _append_tunnel(tunnels, srcs[0], p1, s) + for t, p2 in zip(tgts, tgt_pts): + if mixed_oc and t.box and t.side == "C": + drop_x = max(p2[0], p1[0] + ch.stub) + pts = _simplify_ortho([p1, (drop_x, p1[1]), p2]) + elif mixed_oc and t.box is None and t.side == "O": + pts = _simplify_ortho([p1, (work.r, p1[1])]) + else: + pts = _route_icom( + srcs[0], t, p1, p2, ch, geoms, order, arrow, + lane_y=lane_y, spine_x=spine_x, + ) + paths.append(pts) + heads.append((p2, _dir_for_into(t.side) if t.box else _dir_for_out(t.side))) + _append_tunnel(tunnels, t, p2, s) + elif len(tgts) == 1 and len(srcs) > 1: + p2 = _end_point(tgts[0], arrow, geoms, work) + for src in srcs: + p1 = _end_point(src, arrow, geoms, work, mate=tgts[0]) + pts = _route_icom(src, tgts[0], p1, p2, ch, geoms, order, arrow) + paths.append(pts) + _append_tunnel(tunnels, src, p1, s) + heads.append((p2, _dir_for_into(tgts[0].side) if tgts[0].box else _dir_for_out(tgts[0].side))) + _append_tunnel(tunnels, tgts[0], p2, s) + else: + return [] + + seen: set[tuple] = set() + for pts in paths: + for a, b in zip(pts, pts[1:]): + key = _seg_key(a, b) + if key in seen: + continue + seen.add(key) + scene.line(a[0], a[1], b[0], b[1], lw) + for tip, direction in heads: + _arrowhead(scene, tip, direction, size=8 * s, lw=lw) + for pt, horiz in tunnels: + _draw_tunnel(scene, pt, horiz, s, lw) + return paths + + +def _draw_purpose(scene: _Scene, d: Idef0Diagram, work: _Rect, s: float, fonts: dict) -> _Rect | None: + """Kept for callers; kit form does not put Purpose/Viewpoint in the work area.""" + bits = [] + if d.purpose: + bits.append(f"Purpose: {d.purpose}") + if d.viewpoint: + bits.append(f"Viewpoint: {d.viewpoint}") + if not bits: + return None + font = _load_font(int(fonts["small"]), False) + lines: list[str] = [] + for text in bits: + lines.extend(_wrap(text, font, int(work.w * 0.40))) + if not lines: + return None + pad = 3 * s + tw, th = _label_block_size(lines, font, fonts["small"], pad) + halo = _Rect(work.x + 4 * s, work.b - th - 4 * s, tw, th) + scene.rect(halo.x, halo.y, halo.w, halo.h, width=0, fill="#ffffff", outline=None) + line_h = fonts["small"] * 1.15 + y0 = halo.y + pad + fonts["small"] * 0.2 + for i, ln in enumerate(lines): + scene.text(halo.x + pad, y0 + i * line_h, ln, fonts["small"], anchor="lt") + return halo.inflate(4 * s) + + +def render_idef0( + diagram: Idef0Diagram, + *, + scale: float = 2.0, + want_svg: bool = True, +) -> tuple[bytes, str | None]: + """Return (png_bytes, svg_text_or_None).""" + s = max(float(scale), 1.0) + n = max(1, len(diagram.boxes)) + if n == 1: + W, H = int(1200 * s), int(850 * s) + elif n <= 3: + W, H = int(1400 * s), int(920 * s) + else: + W, H = int(1860 * s), int(1280 * s) + scene = _Scene(W, H) + lw = max(1.0, 1.0 * s) + fonts = { + "lab": 9 * s, + "val": 11 * s, + "small": 9.5 * s, + "box": 13 * s, + "id": 10 * s, + "arrow": 12 * s, + "node": 20 * s, + "title": 16 * s, + "top": 22 * s, + } + outer = _Rect(8 * s, 8 * s, W - 16 * s, H - 16 * s) + form = (diagram.form or "kit").strip().lower() + if form == "plain": + scene.rect(outer.x, outer.y, outer.w, outer.h, width=lw) + work = _Rect(outer.x + 20 * s, outer.y + 20 * s, outer.w - 40 * s, outer.h - 40 * s) + else: + work = _draw_kit_form(scene, diagram, outer, s, fonts) + + geoms = _layout_boxes(diagram, work, s) + _assign_slots(diagram, geoms) + order = _box_order(diagram) + ch = _make_channels(diagram, geoms, work, s) + occupied: list[_Rect] = [g.rect.inflate(12 * s) for g in geoms.values()] + boxes = [g.rect for g in geoms.values()] + + labeled: list[tuple[Idef0Arrow, list[list[tuple[float, float]]]]] = [] + all_paths: list[list[tuple[float, float]]] = [] + for arrow in diagram.arrows: + paths = _draw_arrow(scene, arrow, geoms, work, s, lw, order, ch) + labeled.append((arrow, paths)) + all_paths.extend(paths) + shafts = [ + _seg_obstacle(a, b, 6 * s) + for pts in all_paths + for a, b in zip(pts, pts[1:]) + ] + # ICOM codes at the frame first so labels keep off them. + for arrow, _paths in labeled: + _draw_icom_code(scene, arrow, geoms, work, s, fonts, occupied) + for arrow, paths in labeled: + if arrow.label and paths: + pts = _best_label_polyline(paths, min_len=24 * s) + _draw_arrow_label( + scene, pts, arrow.label, s, lw, fonts, work, occupied, boxes, shafts, + ) + for geom in geoms.values(): + _draw_box(scene, geom, s, lw, fonts) + + png = scene.to_png() + svg = scene.to_svg() if want_svg else None + return png, svg + + +def write_idef0_outputs( + source: str, + out_png: Path, + out_svg: Path | None, + *, + scale: float = 2.0, +) -> bool: + """Parse + render. Raises Idef0ParseError on bad DSL.""" + diagram = parse_idef0(source) + png, svg = render_idef0(diagram, scale=scale, want_svg=out_svg is not None) + out_png.parent.mkdir(parents=True, exist_ok=True) + out_png.write_bytes(png) + if out_svg is not None and svg is not None: + out_svg.write_text(svg, encoding="utf-8") + _log.info("IDEF0 ok → %s", out_png.name) + return True + + +# Copy-paste sample for the classic A-0 context diagram. +SAMPLE_CONTEXT = """\ +title Распорядиться товаром +node A-0 +number 1 +author Кинзябулатов Рамиль +project Разработка универсальной модели торгового предприятия +date 08.02.2022 +rev 31.03.2022 +status PUBLICATION +context TOP + +[A0] Распорядиться товаром +<- Спрос +^ Нормативная документация +-> Товар +v Персонал +""" + +# A0 child of the context: 3-box Kinzyabulatov density (fork, O→C, ICOM at border). +SAMPLE_DECOMPOSITION = """\ +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] +""" diff --git a/md2gost/page_geometry.py b/md2gost/page_geometry.py index f7ab65c..f86ca39 100644 --- a/md2gost/page_geometry.py +++ b/md2gost/page_geometry.py @@ -2,13 +2,22 @@ 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) @@ -91,6 +100,10 @@ def apply_section_geometry(section, *, landscape: bool) -> None: 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.""" @@ -131,31 +144,31 @@ def clear_section_footer(section) -> None: 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). """ - from docx.oxml.ns import qn - sections = list(document.sections) if not sections: return - # Heuristic: sections before the first that already has a PAGE field / - # or first N-1 if title was prepended — clear footer on all but keep - # continuous pgNumType. Body renderer already puts PAGE on body section; - # after compose, early sections may inherit footers — clear empty ones - # that belong to front matter (no Heading-like body content is hard to - # detect), so: clear footer on every section that has no PAGE field, and - # strip w:pgNumType start=1 everywhere. + # Strip w:pgNumType start everywhere so landscape / body sections stay continuous. for section in sections: - sect_pr = section._sectPr - for child in list(sect_pr): - if child.tag == qn("w:pgNumType"): - # Keep continuous: remove start attribute if present - if child.get(qn("w:start")) is not None: - del child.attrib[qn("w:start")] + clear_section_page_start(section) def set_section_page_start(section, start: int) -> None: @@ -191,3 +204,100 @@ def apply_page_number_start( 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 diff --git a/md2gost/pipeline.py b/md2gost/pipeline.py index 62bb9b3..82cfbca 100644 --- a/md2gost/pipeline.py +++ b/md2gost/pipeline.py @@ -9,7 +9,7 @@ import subprocess import traceback from dataclasses import dataclass from datetime import datetime -from getpass import getuser +from pathlib import Path from typing import Callable from docx import Document @@ -41,6 +41,10 @@ class ConvertRequest: 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 @@ -58,6 +62,15 @@ class ConvertRequest: 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 @@ -127,16 +140,33 @@ def _apply_page_offset(document, req: ConvertRequest, *, had_title: bool, had_as from .page_geometry import apply_page_number_start start = getattr(req, "page_number_start", None) + start_i: int | None if start is None: - return - try: - start_i = int(start) - except (TypeError, ValueError): - return + 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 @@ -154,6 +184,24 @@ def convert(req: ConvertRequest, log: LogFn | None = None) -> ConvertResult: 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"): @@ -192,10 +240,57 @@ def convert(req: ConvertRequest, log: LogFn | None = None) -> ConvertResult: 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( - md_text, + body_md, req.doc_type, table_continuation=req.table_continuation, listing_continuation=req.listing_continuation, @@ -211,9 +306,6 @@ def convert(req: ConvertRequest, log: LogFn | None = None) -> ConvertResult: message=check_report, ) - if not output: - output = default_output_path(filename) - template = (req.template or "").strip() or default_template_path() handler = _CallbackLogHandler(emit) @@ -236,11 +328,12 @@ def convert(req: ConvertRequest, log: LogFn | None = None) -> ConvertResult: 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 req.title or req.assignment: + if title_path or req.assignment: try: from docxcompose.composer import Composer except ImportError: @@ -256,8 +349,8 @@ def convert(req: ConvertRequest, log: LogFn | None = None) -> ConvertResult: body.remove(child) composer = Composer(shell) - if req.title: - composer.append(Document(req.title)) + if title_path: + composer.append(Document(title_path)) shell.add_page_break() if req.assignment: composer.append(Document(req.assignment)) @@ -267,19 +360,36 @@ def convert(req: ConvertRequest, log: LogFn | None = None) -> ConvertResult: apply_style_config(document, style_cfg) _fix_front_matter_after_compose( document, - had_title=bool(req.title), + had_title=bool(title_path), had_assignment=bool(req.assignment), ) _apply_page_offset( document, req, - had_title=bool(req.title), + had_title=bool(title_path), had_assignment=bool(req.assignment), ) - document.core_properties.author = getuser() - document.core_properties.comments = "Создано при помощи md2gost (ТЗ МИРЭА)" + 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: @@ -297,12 +407,19 @@ def convert(req: ConvertRequest, log: LogFn | None = None) -> ConvertResult: 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"), @@ -317,6 +434,16 @@ def convert(req: ConvertRequest, log: LogFn | None = None) -> ConvertResult: 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 diff --git a/md2gost/profiles.py b/md2gost/profiles.py index 7c651c6..9b6de6c 100644 --- a/md2gost/profiles.py +++ b/md2gost/profiles.py @@ -99,7 +99,7 @@ def get_profile(doc_type: str) -> DocProfile: # --- text autofixes --- -# ```uml / ```mermaid / ```uml-c4 / ```bpmn … — кавычки и тире там синтаксис +# ```uml / ```mermaid / ```uml-c4 … — кавычки и тире там синтаксис _FENCE_RE = re.compile( r"^(?P`{3,}|~{3,})(?P[^\n]*)\n" r"(?P[\s\S]*?)" diff --git a/md2gost/renderable/paragraph.py b/md2gost/renderable/paragraph.py index 3cedcde..036071f 100644 --- a/md2gost/renderable/paragraph.py +++ b/md2gost/renderable/paragraph.py @@ -293,7 +293,9 @@ class Paragraph(Renderable): yield SubRenderable(image, False) layout_state.add_height(rendered_image_height) else: + # Defer this image and any remaining siblings to the next page + # (as SubRenderable — bare Image objects break the renderer). yield SubRenderable(image, True) - break - - yield from images + for rest in images: + yield SubRenderable(rest, True) + return diff --git a/md2gost/renderable/table.py b/md2gost/renderable/table.py index 51c645d..325ce3d 100644 --- a/md2gost/renderable/table.py +++ b/md2gost/renderable/table.py @@ -113,6 +113,10 @@ class Table(Renderable, RequiresNumbering): cell_height = 0 if not (apply_merge and merge_v == "continue"): for paragraph in row[col]: + # Method guide §3: do not leave caption + column headers alone + # at page bottom — glue header row to the following data row. + if row_idx == 0: + paragraph._docx_paragraph.paragraph_format.keep_with_next = True cell_layout_state = LayoutState( layout_state.max_height, layout_state.max_width ) @@ -133,6 +137,62 @@ class Table(Renderable, RequiresNumbering): ) return docx_row, row_height + def _peek_row_height(self, row_idx: int, layout_state: LayoutState) -> Length: + """Estimate row height without attaching cell paragraphs to a table.""" + from .paragraph_sizer import ParagraphSizer + + row = self._rows[row_idx] + merge_row = self._merge[row_idx] + row_height = 0 + col_w = self._col_width(layout_state) + col = 0 + while col < self._cols: + merge_v, merge_h = merge_row[col] + if merge_h == "continue": + col += 1 + continue + span = self._grid_span(merge_row, col) + if span < 1: + span = 1 + cell_height = 0 + if merge_v != "continue": + max_w = col_w * span - CELL_OFFSET + for paragraph in row[col]: + cell_height += ParagraphSizer( + paragraph._docx_paragraph, None, max_w + ).calculate_height().full + row_height = max(cell_height, row_height) + col += span + row_height += Pt(0.5) + ( + ROW_HEIGHT_SLACK if self._continuation_mode not in _WORD_PAGED_MODES else Pt(0) + ) + return row_height + + def _start_block_height( + self, previous_rendered: RenderedInfo | None, layout_state: LayoutState + ) -> Length: + """Caption + header + first data row (method: no orphan title/header).""" + from docx.text.paragraph import Paragraph as DocxParagraph + from .paragraph_sizer import ParagraphSizer + + # Dry-run caption sizing (temporary Caption; not yielded into the body). + cap = Caption( + self._parent, "Таблица", self._caption_info, self._number, True + ) + prev_p = ( + previous_rendered.docx_element + if previous_rendered and isinstance(previous_rendered.docx_element, DocxParagraph) + else None + ) + total = ParagraphSizer( + cap._docx_paragraph, prev_p, layout_state.max_width + ).calculate_height().full + if self._rows: + total += self._peek_row_height(0, layout_state) + if len(self._rows) > 1: + total += self._peek_row_height(1, layout_state) + return total + def _should_split_fragment( self, row_height: Length, layout_state: LayoutState, rows_in_fragment: int ) -> bool: @@ -169,10 +229,29 @@ class Table(Renderable, RequiresNumbering): def render(self, previous_rendered: RenderedInfo, layout_state: LayoutState) \ -> Generator[RenderedInfo, None, None]: + # Method guide: never leave only «Таблица N» or «Таблица N» + column + # headers at the bottom of a page. If caption+header+first data row do + # not fit, push the whole block to the next page (layout leftover; + # Word also gets keep_with_next on caption/header). + leftover = Pt(0) + start_h = self._start_block_height(previous_rendered, layout_state) + if ( + layout_state.current_page_height > 0 + and start_h > layout_state.remaining_page_height + ): + leftover = layout_state.remaining_page_height + layout_state.add_height(leftover) + previous_rendered = None + caption_rendered_infos = list( Caption(self._parent, "Таблица", self._caption_info, self._number, True) .render(previous_rendered, copy(layout_state)) ) + if leftover and caption_rendered_infos: + first = caption_rendered_infos[0] + caption_rendered_infos[0] = RenderedInfo( + first.docx_element, first.height + leftover + ) layout_state.add_height(sum(info.height for info in caption_rendered_infos)) yield from caption_rendered_infos @@ -190,19 +269,22 @@ class Table(Renderable, RequiresNumbering): ) if self._should_split_fragment(row_height, layout_state, rows_in_fragment): - yield RenderedInfo(docx_table, table_height) - yield from self._emit_page_break_and_optional_caption(layout_state) + # Never cut after the header alone (method: no title+column headers + # without a data row). Prefer slight overflow over an orphan fragment. + if rows_in_fragment >= 2: + yield RenderedInfo(docx_table, table_height) + yield from self._emit_page_break_and_optional_caption(layout_state) - docx_table = create_table( - self._parent, 0, self._cols, self._table_width(layout_state) - ) - apply_merge = False - rows_in_fragment = 0 - table_height = Pt(0.5) + docx_table = create_table( + self._parent, 0, self._cols, self._table_width(layout_state) + ) + apply_merge = False + rows_in_fragment = 0 + table_height = Pt(0.5) - docx_row, row_height = self._build_docx_row( - docx_table, row_idx, apply_merge, layout_state - ) + docx_row, row_height = self._build_docx_row( + docx_table, row_idx, apply_merge, layout_state + ) docx_table._element.append(docx_row._element) layout_state.add_height(row_height) diff --git a/md2gost/renderer.py b/md2gost/renderer.py index a47c570..45f40de 100644 --- a/md2gost/renderer.py +++ b/md2gost/renderer.py @@ -122,6 +122,9 @@ class Renderer: def process(self, renderables: list[Renderable]): items = self._ensure_appendix_index(list(renderables)) for i in range(len(items)): + # Figures that did not fit the previous page must land before the + # next top-level block (not after a later table / end of doc). + self._flush_to_new_screen() self.render(items[i]) self._flush_to_new_screen() diff --git a/md2gost/title_page.py b/md2gost/title_page.py new file mode 100644 index 0000000..ba4cc22 --- /dev/null +++ b/md2gost/title_page.py @@ -0,0 +1,323 @@ +"""Generate MIREA practice title page from TitleTemplate.docx + meta sources. + +Red runs (w:color C00000/FF0000) in the bundled template are placeholders. +Sources (later wins for academic fields): info_conv.yaml < ```title fence. +Student ФИО/группа come from user profile / CLI, not from yaml. +""" + +from __future__ import annotations + +import logging +import os +import re +import shutil +from dataclasses import dataclass, fields +from datetime import datetime +from pathlib import Path + +from docx import Document +from docx.oxml.ns import qn +from docx.shared import RGBColor + +from . import package_dir + +_log = logging.getLogger(__name__) + +INFO_CONV_FILENAME = "info_conv.yaml" +TITLE_FENCE_RE = re.compile( + r"^```title[ \t]*\r?\n(.*?)(?:\r?\n)```[ \t]*(?:\r?\n|$)", + re.DOTALL | re.IGNORECASE | re.MULTILINE, +) +KV_LINE_RE = re.compile(r"^([^\s:#][^:#]*?)\s*:\s*(.*)$") + +# Canonical field → accepted key aliases (lowercased for match) +_KEY_ALIASES: dict[str, tuple[str, ...]] = { + "institute": ( + "institute", + "институт", + "инститиут", + ), + "department": ( + "department", + "кафедра", + ), + "discipline": ( + "discipline", + "дисциплина", + ), + "teacher": ( + "teacher", + "преподаватель", + "преподователь", + ), + "teacher_position": ( + "teacher_position", + "teacherposition", + "должность", + "должность преподавателя", + ), + "number": ("number", "номер", "№", "n"), + "year": ("year", "год"), + "student": ("student", "фио", "студент"), + "group": ("group", "группа"), +} + +# Paragraph indices in bundled TitleTemplate.docx (do not reorder template). +# P0 institute, P1 department, P5 discipline (red inside guillemets), +# P8 number, P13 group, P15 student, P19 teacher_position, P21 teacher, P28 year. +_SLOT_PARAS: dict[str, int] = { + "institute": 0, + "department": 1, + "discipline": 5, + "number": 8, + "group": 13, + "student": 15, + "teacher_position": 19, + "teacher": 21, + "year": 28, +} + +_RED_VALS = frozenset({"FF0000", "C00000", "FF0", "F00"}) + + +@dataclass +class TitleMeta: + institute: str = "" + department: str = "" + discipline: str = "" + number: str = "" + group: str = "" + student: str = "" + teacher_position: str = "" + teacher: str = "" + year: str = "" + + def academic_filled(self) -> bool: + return any( + [ + self.institute, + self.department, + self.discipline, + self.teacher, + self.teacher_position, + self.number, + self.year, + ] + ) + + +def default_title_template_path() -> str: + return os.path.join(package_dir(), "TitleTemplate.docx") + + +def _norm_key(key: str) -> str: + return re.sub(r"\s+", " ", (key or "").strip().lower().replace("ё", "е")) + + +def _alias_to_field(key: str) -> str | None: + nk = _norm_key(key) + for field_name, aliases in _KEY_ALIASES.items(): + if nk in aliases: + return field_name + return None + + +def parse_kv_text(text: str) -> dict[str, str]: + """Parse flat `key: value` lines (title fence / info_conv.yaml).""" + out: dict[str, str] = {} + for raw in (text or "").splitlines(): + line = raw.strip() + if not line or line.startswith("#"): + continue + if line in ("---", "..."): + continue + m = KV_LINE_RE.match(line) + if not m: + continue + field_name = _alias_to_field(m.group(1)) + if not field_name: + continue + value = m.group(2).strip().strip("\"'") + # strip inline comment + if " #" in value: + value = value.split(" #", 1)[0].rstrip() + if value: + out[field_name] = value + return out + + +def parse_and_strip_title_fence(md: str) -> tuple[dict[str, str], str]: + """Return (fields from first ```title fence, markdown without that fence).""" + m = TITLE_FENCE_RE.search(md or "") + if not m: + return {}, md or "" + fields_map = parse_kv_text(m.group(1)) + body = (md or "")[: m.start()] + (md or "")[m.end() :] + # collapse leading blank lines left by stripping + body = re.sub(r"^\s*\n", "", body, count=1) + return fields_map, body + + +def load_info_conv(md_dir: str | Path) -> dict[str, str]: + path = Path(md_dir) / INFO_CONV_FILENAME + if not path.is_file(): + return {} + try: + text = path.read_text(encoding="utf-8") + except OSError as exc: + _log.warning("Не удалось прочитать %s: %s", path, exc) + return {} + return parse_kv_text(text) + + +def info_conv_exists(md_dir: str | Path) -> bool: + return (Path(md_dir) / INFO_CONV_FILENAME).is_file() + + +def merge_title_meta( + *, + yaml_fields: dict[str, str] | None = None, + fence_fields: dict[str, str] | None = None, + student: str = "", + group: str = "", +) -> TitleMeta: + """Fence overrides yaml for academic fields; student/group from args.""" + merged: dict[str, str] = {} + for src in (yaml_fields or {}, fence_fields or {}): + for k, v in src.items(): + if k in ("student", "group"): + continue # never from yaml/fence for title identity + if v: + merged[k] = v + meta = TitleMeta(**{f.name: merged.get(f.name, "") for f in fields(TitleMeta)}) + meta.student = (student or "").strip() + meta.group = (group or "").strip() + if not meta.year: + meta.year = str(datetime.now().year) + if not meta.number: + meta.number = "1" + return meta + + +def _run_color_val(run) -> str | None: + rPr = run._element.find(qn("w:rPr")) + if rPr is None: + return None + c = rPr.find(qn("w:color")) + if c is None: + return None + return (c.get(qn("w:val")) or "").upper() or None + + +def _is_red_run(run) -> bool: + val = _run_color_val(run) + if not val: + return False + if val in _RED_VALS: + return True + # theme-ish: FF???? with green/blue near zero + if len(val) == 6 and val.startswith("FF") and val[2:4] == "00" and val[4:6] == "00": + return True + if len(val) == 6 and val.startswith("C0") and val[2:] == "0000": + return True + return val == "C00000" + + +def _clear_run_color(run) -> None: + rPr = run._element.find(qn("w:rPr")) + if rPr is None: + return + c = rPr.find(qn("w:color")) + if c is not None: + rPr.remove(c) + try: + run.font.color.rgb = RGBColor(0, 0, 0) + except Exception: + pass + + +def _replace_red_in_paragraph(paragraph, value: str) -> bool: + """Replace contiguous red runs' text with ``value``; clear red color.""" + runs = list(paragraph.runs) + red_idxs = [i for i, r in enumerate(runs) if _is_red_run(r) and (r.text or "")] + if not red_idxs: + # also consider empty/tab-only already captured + red_idxs = [i for i, r in enumerate(runs) if _is_red_run(r)] + if not red_idxs: + return False + first = red_idxs[0] + runs[first].text = value + _clear_run_color(runs[first]) + for i in red_idxs[1:]: + runs[i].text = "" + _clear_run_color(runs[i]) + return True + + +def fill_title_document(doc: Document, meta: TitleMeta) -> None: + """Fill red placeholder slots in an opened title template document.""" + paras = doc.paragraphs + mapping = { + "institute": meta.institute, + "department": meta.department, + "discipline": meta.discipline, + "number": meta.number, + "group": meta.group, + "student": meta.student, + "teacher_position": meta.teacher_position, + "teacher": meta.teacher, + "year": meta.year, + } + for field_name, idx in _SLOT_PARAS.items(): + value = (mapping.get(field_name) or "").strip() + if not value: + continue + if idx >= len(paras): + _log.warning("TitleTemplate: нет абзаца %s для поля %s", idx, field_name) + continue + if not _replace_red_in_paragraph(paras[idx], value): + # Fallback: whole paragraph is placeholder (student name) + if field_name in ("student", "institute", "department") and paras[idx].runs: + paras[idx].runs[0].text = value + for r in paras[idx].runs[1:]: + r.text = "" + _clear_run_color(paras[idx].runs[0]) + else: + _log.warning( + "TitleTemplate: не найдены красные runs в P%s (%s)", idx, field_name + ) + + +def build_title_docx( + meta: TitleMeta, + out_path: str | Path, + *, + template_path: str | Path | None = None, +) -> Path: + """Copy template, fill slots, save to out_path. Returns out_path.""" + src = Path(template_path or default_title_template_path()) + if not src.is_file(): + raise FileNotFoundError(f"Шаблон титульника не найден: {src}") + dest = Path(out_path) + dest.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(src, dest) + doc = Document(str(dest)) + fill_title_document(doc, meta) + doc.save(str(dest)) + return dest + + +def should_generate_title( + *, + explicit_title: str | None, + fence_fields: dict[str, str], + md_dir: str | Path, + enabled: bool = False, +) -> bool: + if not enabled: + return False + if explicit_title and str(explicit_title).strip(): + return False + if fence_fields: + return True + return info_conv_exists(md_dir) diff --git a/md2gost/user_profile.py b/md2gost/user_profile.py new file mode 100644 index 0000000..a81686d --- /dev/null +++ b/md2gost/user_profile.py @@ -0,0 +1,60 @@ +"""Persistent student profile (ФИО + группа) and DOCX metadata defaults.""" + +from __future__ import annotations + +import json +import logging +from dataclasses import dataclass, field +from pathlib import Path + +from .diagram_includes import app_dir +from .doc_metadata import DocumentMetadata + +_log = logging.getLogger(__name__) + +USER_PROFILE_FILENAME = "md2gost.user.json" + + +@dataclass +class UserProfile: + student: str = "" + group: str = "" + metadata: DocumentMetadata = field(default_factory=DocumentMetadata) + + def is_complete(self) -> bool: + return bool(self.student.strip() and self.group.strip()) + + +def user_profile_path(base: Path | None = None) -> Path: + return (base or app_dir()) / USER_PROFILE_FILENAME + + +def load_user_profile(base: Path | None = None) -> UserProfile: + path = user_profile_path(base) + if not path.is_file(): + return UserProfile() + try: + raw = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + _log.warning("Не удалось прочитать %s: %s", path, exc) + return UserProfile() + if not isinstance(raw, dict): + return UserProfile() + return UserProfile( + student=str(raw.get("student") or "").strip(), + group=str(raw.get("group") or "").strip(), + metadata=DocumentMetadata.from_dict(raw.get("metadata")), + ) + + +def save_user_profile(profile: UserProfile, base: Path | None = None) -> Path: + path = user_profile_path(base) + path.parent.mkdir(parents=True, exist_ok=True) + meta = profile.metadata or DocumentMetadata() + data = { + "student": (profile.student or "").strip(), + "group": (profile.group or "").strip(), + "metadata": meta.to_dict(), + } + path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + return path diff --git a/md2gost/word_fix.py b/md2gost/word_fix.py index e52f669..5360861 100644 --- a/md2gost/word_fix.py +++ b/md2gost/word_fix.py @@ -81,6 +81,24 @@ def find_page_break_row(page_numbers: list[int]) -> int | None: return None +def is_orphan_header_break(break_at: int | None, *, had_header: bool) -> bool: + """True if a Word split would leave only the heading row on the previous page. + + Method guide §3: do not leave «Таблица N» + column headers alone at page end. + """ + return bool(had_header and break_at == 2) + + +def is_caption_orphaned(caption_page: int | None, first_row_page: int | None) -> bool: + """True if «Таблица N — …» sits on an earlier page than the table body. + + Method guide: do not leave only the table title at the bottom of a page. + """ + if caption_page is None or first_row_page is None: + return False + return caption_page < first_row_page + + def continuation_label(kind: str, number: str) -> str: if kind == "listing": return f"Продолжение Листинга {number}" @@ -146,6 +164,11 @@ def fix_continuations( splits += made skipped = sum(1 for d in details if d.startswith("skip:")) + _clear_section_page_restarts(doc) + try: + doc.Fields.Update() + except Exception as exc: + details.append(f"skip: fields update: {exc}") doc.Save() msg = ( f"Пост-разрыв Word: разрезов {splits}" @@ -175,25 +198,260 @@ def fix_continuations( pass +def _clear_section_page_restarts(doc) -> None: + """Turn off page-number restart on every section (continuous PAGE through landscape).""" + # wdHeaderFooterPrimary = 1, wdHeaderFooterFirstPage = 2, wdHeaderFooterEvenPages = 3 + for i in range(1, int(doc.Sections.Count) + 1): + sec = doc.Sections(i) + for hf_kind in (1, 2, 3): + for getter in (sec.Headers, sec.Footers): + try: + hf = getter(hf_kind) + except Exception: + continue + try: + pns = hf.PageNumbers + except Exception: + continue + try: + if int(pns.Count) < 1: + continue + except Exception: + continue + try: + pns.RestartNumberingAtSection = False + except Exception: + pass + + def _row_page_numbers(table) -> list[int]: - """Page number at the start of each row (more reliable than end-of-range).""" + """Page number at the start of each row (more reliable than end-of-range). + + Tables with vertical merges (`^` in md2gost) often raise on ``Rows(i).Range``; + fall back to ``Cell(i, j).Range`` per column. + """ pages: list[int] = [] n = int(table.Rows.Count) for i in range(1, n + 1): - try: - rng = table.Rows(i).Range - # Collapse to start so a tall row reports where it begins - start = int(rng.Start) - page = int(rng.Document.Range(start, start).Information(_WD_ACTIVE_END_PAGE_NUMBER)) - except Exception: - try: - page = int(table.Rows(i).Range.Information(_WD_ACTIVE_END_PAGE_NUMBER)) - except Exception: - page = pages[-1] if pages else 1 + page = _page_at_row(table, i) + if page is None: + page = pages[-1] if pages else 1 pages.append(page) return pages +def _page_at_row(table, row_1based: int) -> int | None: + """Best-effort page number for a table row (handles vertical merges).""" + # Prefer Cells — works when Rows(i) is blocked by vMerge. + for j in range(1, 32): + try: + start = int(table.Cell(row_1based, j).Range.Start) + return int( + table.Range.Document.Range(start, start).Information( + _WD_ACTIVE_END_PAGE_NUMBER + ) + ) + except Exception: + # Missing cell in this column (merge continue) or past last column + if j == 1: + continue + # After first column, consecutive failures → stop scanning columns + break + try: + rng = table.Rows(row_1based).Range + start = int(rng.Start) + return int( + rng.Document.Range(start, start).Information(_WD_ACTIVE_END_PAGE_NUMBER) + ) + except Exception: + try: + return int(table.Rows(row_1based).Range.Information(_WD_ACTIVE_END_PAGE_NUMBER)) + except Exception: + return None + + +def _probe_ncols(table) -> int: + """Max column index reachable from row 1 (and a few other rows).""" + ncols = 1 + for row in (1, 2, 3): + for probe in range(1, 64): + try: + table.Cell(row, probe) + ncols = max(ncols, probe) + except Exception: + break + return max(ncols, 1) + + +def _rows_individually_accessible(table) -> bool: + """False when Word blocks ``Rows(i)`` due to vertical merges.""" + try: + n = int(table.Rows.Count) + if n < 1: + return True + _ = table.Rows(1).Range + if n > 1: + _ = table.Rows(n).Range + return True + except Exception: + return False + + +def _capture_vmerge_spans(table) -> list[tuple[int, int, int, str]]: + """Return ``(row, col, span, text)`` for each vertical merge (1-based, span>1).""" + try: + nrows = int(table.Rows.Count) + except Exception: + return [] + ncols = _probe_ncols(table) + spans: list[tuple[int, int, int, str]] = [] + r = 1 + while r <= nrows: + for c in range(1, ncols + 1): + try: + cell = table.Cell(r, c) + except Exception: + continue + span = 1 + while r + span <= nrows: + try: + table.Cell(r + span, c) + break + except Exception: + row_exists = False + for cc in range(1, ncols + 1): + try: + table.Cell(r + span, cc) + row_exists = True + break + except Exception: + pass + if not row_exists: + break + span += 1 + if span > 1: + spans.append((r, c, span, _cell_plain_text(cell))) + r += 1 + return spans + + +def split_vmerge_spans_at_break( + spans: list[tuple[int, int, int, str]], + break_at: int, +) -> tuple[list[tuple[int, int, int, str]], list[tuple[int, int, int, str]]]: + """Split captured merges across a 1-based ``Table.Split`` row index.""" + first: list[tuple[int, int, int, str]] = [] + cont: list[tuple[int, int, int, str]] = [] + for r, c, span, text in spans: + if span <= 1: + continue + end = r + span - 1 + if end < break_at: + first.append((r, c, span, text)) + elif r >= break_at: + cont.append((r - break_at + 1, c, span, text)) + else: + top = break_at - r + if top > 1: + first.append((r, c, top, text)) + bottom = end - break_at + 1 + if bottom >= 1: + cont.append((1, c, bottom, text)) + return first, cont + + +def _set_cell_plain_text(cell, text: str) -> None: + """Write plain text into a cell without eating the end-of-cell mark badly.""" + raw = (text or "").replace("\r", "").replace("\x07", "").replace("\a", "") + try: + rng = cell.Range.Duplicate + # Exclude the cell's terminal marker characters + if int(rng.End) > int(rng.Start) + 1: + rng.End = int(rng.End) - 1 + rng.Text = raw + except Exception: + try: + cell.Range.Text = raw + except Exception: + pass + + +def _restore_vmerge_spans(table, spans: list[tuple[int, int, int, str]]) -> int: + """Re-apply vertical merges after unmerge+Split. Returns merges restored.""" + made = 0 + for r, c, span, text in spans: + if span <= 1: + continue + try: + nrows = int(table.Rows.Count) + if r < 1 or r > nrows or r + span - 1 > nrows: + continue + start = table.Cell(r, c) + if text and not _cell_plain_text(start): + _set_cell_plain_text(start, text) + if span == 1: + made += 1 + continue + end = table.Cell(r + span - 1, c) + start.Merge(end) + made += 1 + except Exception: + continue + return made + + +def _unmerge_vertical_cells(table) -> int: + """Split vertically merged cells so ``Table.Split`` can run. + + Returns number of cell-groups unmerged. Safe no-op if nothing to do. + Prefer capturing spans with ``_capture_vmerge_spans`` first and restoring + after the table split. + """ + try: + nrows = int(table.Rows.Count) + except Exception: + return 0 + ncols = _probe_ncols(table) + + made = 0 + r = 1 + while r <= nrows: + for c in range(1, ncols + 1): + try: + cell = table.Cell(r, c) + except Exception: + continue + span = 1 + while r + span <= nrows: + try: + table.Cell(r + span, c) + break + except Exception: + row_exists = False + for cc in range(1, ncols + 1): + try: + table.Cell(r + span, cc) + row_exists = True + break + except Exception: + pass + if not row_exists: + break + span += 1 + if span > 1: + try: + cell.Split(span, 1) + made += 1 + except Exception: + pass + r += 1 + try: + nrows = int(table.Rows.Count) + except Exception: + break + return made + + def _paragraph_page_numbers_in_cell(table) -> list[int]: """Fallback: page of each paragraph in a single-cell table (legacy 1-row listings).""" try: @@ -245,6 +503,25 @@ def _expand_single_cell_listing_to_rows(table) -> bool: return False +def _paragraph_page(paragraph) -> int | None: + try: + start = int(paragraph.Range.Start) + return int( + paragraph.Range.Document.Range(start, start).Information( + _WD_ACTIVE_END_PAGE_NUMBER + ) + ) + except Exception: + return None + + +def _caption_page_before_table(table) -> int | None: + p = _paragraph_before_table(table) + if p is None: + return None + return _paragraph_page(p) + + def _fix_pass( doc, *, @@ -285,18 +562,81 @@ def _fix_pass( doc.Repaginate() pages = _row_page_numbers(table) + # Method: only the title («Таблица N») at page bottom is forbidden. + if caption.kind == "table" and pages: + if is_caption_orphaned(_caption_page_before_table(table), pages[0]): + try: + if _page_break_before_table_caption(table): + details.append( + f"orphan-caption: {caption.kind} {caption.number} " + f"→ page break before caption" + ) + doc.Repaginate() + else: + details.append( + f"skip: orphan-caption {caption.kind} {caption.number}: " + f"no caption para" + ) + except Exception as exc: + details.append(f"skip: orphan-caption page break: {exc}") + continue + break_at = find_page_break_row(pages) if break_at is None: continue if break_at > int(table.Rows.Count): continue + # Vertical merges block Rows()/Table.Split — unmerge, split, then restore. + vmerge_spans: list[tuple[int, int, int, str]] = [] + if not _rows_individually_accessible(table): + try: + vmerge_spans = _capture_vmerge_spans(table) + n_un = _unmerge_vertical_cells(table) + if n_un: + details.append( + f"unmerge: {caption.kind} {caption.number} ({n_un} groups)" + ) + doc.Repaginate() + table = doc.Tables(ti) + pages = _row_page_numbers(table) + break_at = find_page_break_row(pages) + if break_at is None: + details.append( + f"skip: {caption.kind} {caption.number} " + f"no page break after unmerge" + ) + continue + if break_at > int(table.Rows.Count): + continue + except Exception as exc: + details.append(f"skip: unmerge {caption.kind} {caption.number}: {exc}") + continue + had_header = False try: had_header = bool(table.Rows(1).HeadingFormat) except Exception: had_header = False + # Method guide: caption + column headers alone on a page is forbidden. + # If Word broke right after the heading row, push the table (caption) to + # the next page instead of splitting into a header-only fragment. + if is_orphan_header_break(break_at, had_header=had_header): + try: + if _page_break_before_table_caption(table): + details.append( + f"orphan-header: {caption.kind} {caption.number} → page break before caption" + ) + doc.Repaginate() + else: + details.append( + f"skip: orphan-header {caption.kind} {caption.number}: no caption para" + ) + except Exception as exc: + details.append(f"skip: orphan-header page break: {exc}") + continue + try: table.Split(break_at) except Exception as exc: @@ -304,12 +644,28 @@ def _fix_pass( continue try: + table = doc.Tables(ti) cont = doc.Tables(ti + 1) except Exception as exc: details.append(f"skip: after split cannot get continuation table[{ti}+1]: {exc}") made += 1 continue + if vmerge_spans: + try: + first_spans, cont_spans = split_vmerge_spans_at_break( + vmerge_spans, break_at + ) + n1 = _restore_vmerge_spans(table, first_spans) + n2 = _restore_vmerge_spans(cont, cont_spans) + if n1 or n2: + details.append( + f"remerge: {caption.kind} {caption.number} " + f"(first={n1}, cont={n2})" + ) + except Exception as exc: + details.append(f"skip: remerge {caption.kind} {caption.number}: {exc}") + before_cont = _paragraph_text_before_table(cont) first_line = before_cont.strip().split("\n", 1)[0] if before_cont else "" if not (first_line and _CONTINUATION_RE.match(first_line)): @@ -332,7 +688,7 @@ def _fix_pass( except Exception as exc: details.append(f"skip: listing borders: {exc}") - # Header repeat is opt-in (default off) + # Header repeat is opt-in (default off) — not Word's native tblHeader paint if repeat_header and had_header and caption.kind == "table": try: _ensure_header_on_continuation(first_table=table, cont_table=cont) @@ -351,19 +707,44 @@ def _caption_before_table(table) -> CaptionInfo | None: return parse_caption_text(text) -def _paragraph_text_before_table(table) -> str: +def _paragraph_before_table(table): + """Return the Word Paragraph immediately before ``table``, or None.""" try: rng = table.Range if rng.Start <= 1: - return "" + return None doc = table.Range.Document prev = doc.Range(rng.Start - 1, rng.Start) - p = prev.Paragraphs(1) + return prev.Paragraphs(1) + except Exception: + return None + + +def _paragraph_text_before_table(table) -> str: + try: + p = _paragraph_before_table(table) + if p is None: + return "" return (p.Range.Text or "").replace("\r", "").replace("\x07", "").strip() except Exception: return "" +def _page_break_before_table_caption(table) -> bool: + """Set PageBreakBefore on the caption paragraph above the table.""" + p = _paragraph_before_table(table) + if p is None: + return False + info = parse_caption_text( + (p.Range.Text or "").replace("\r", "").replace("\x07", "").strip() + ) + if info is None or info.is_continuation: + return False + p.Format.PageBreakBefore = True + p.Format.KeepWithNext = True + return True + + def _insert_continuation_before(doc, table, caption: CaptionInfo) -> None: """Insert caption paragraph *outside* the table (before its start). diff --git a/prompts/README.md b/prompts/README.md index 63b2155..b50e47c 100644 --- a/prompts/README.md +++ b/prompts/README.md @@ -7,12 +7,13 @@ | [`generate-md.md`](generate-md.md) | Диалект md2gost: отличия от обычного MD, диаграммы конвертируются сами | | [`generate-mirea-report.md`](generate-mirea-report.md) | Полный системный промпт (ГОСТ / МИРЭА) | | [`generate-pis-custom-report.md`](generate-pis-custom-report.md) | Итоговый отчёт ПИС | +| [`emulate-student.md`](emulate-student.md) | Голос: человечный учебный текст, не отчёт ИИ (копировать **после** generate-*) | В GUI (**Справка → Промпт для ИИ**) можно дописать к любому промпту выбранные UML-схемы из `md2gost.schemes.json`. ### Как использовать -1. Открой файл или GUI, скопируй промпт (в GUI — после выбора схем). +1. Открой файл или GUI, скопируй промпт (в GUI — после выбора схем). Для живого тона добавь [`emulate-student.md`](emulate-student.md) после базового generate-*. 2. Вставь в: - **ChatGPT / Claude** — Custom instructions / первое сообщение; - **Cursor** — Rules for AI / сообщение агенту; diff --git a/prompts/emulate-student.md b/prompts/emulate-student.md new file mode 100644 index 0000000..beed83c --- /dev/null +++ b/prompts/emulate-student.md @@ -0,0 +1,57 @@ +# Промпт: человечный учебный текст (не отчёт ИИ) + +Скопируйте **весь** текст ниже **после** базового промпта md2gost (`generate-md` / `generate-mirea-report` / `generate-pis-custom-report`) или вторым сообщением. + +Тема, объект, факты — из следующего сообщения пользователя. Этот блок **не привязан** к конкретному предмету: он задаёт только голос. + +--- + +Ты пишешь учебный текст так, чтобы он читался как работа студента-человека, а не как отчёт модели о выполненных командах. + +## Главное + +Пиши **про предмет работы**. Не пиши про то, что задание сделано, пункт закрыт или следующий этап курса уже «зафиксирован». + +Читатель должен узнать объект / тему. Он не должен видеть пайплайн промпта, чеклист ТЗ и разговор с невидимым проверяющим. + +## Как строить кусок текста + +1. Тезис по сути. +2. Опора: таблица, рисунок, число, факт. +3. Короткий вывод **про сам предмет** (1–3 предложения). + +Не начинай раздел преамбулой «зачем этот блок существует». Запрещены обороты вроде: «X является исходным условием…», «без X невозможны…», «что позволяет связать предмет…», «а не подменять…». Сразу давай содержание. + +Факт имеет право просто быть. Не пристёгивай каждое предложение к следующей главе, «предмету исследования», автоматизации, пользе для проверяющего или «прикладной ценности полученной характеристики» — если пользователь сам не сделал это темой. + +## Введение и заключение + +**Введение:** цель → объект / тема → почему взято → методы. Без оглавления остальных работ курса. Без фраз вроде «роботизация закрепляется за…», «предметом последующих работ принимается…», «целевая архитектура должна…». + +**Заключение:** сожми предмет — что это, как устроено, что важно. +Запрещены глаголы сдачи работы: «сформирована характеристика», «установлены», «определён», «построена», «зафиксирован предмет». +Запрещён план следующих лаб вместо вывода по теме. + +## Таблицы + +Таблица описывает предмет. Последняя колонка — смысл в теме (содержание, влияние, назначение в деятельности). +Не делай колонки: «значение для объекта исследования», «роль в предмете исследования», «назначение для характеристики», «как пригодится дальше / для автоматизации». + +## Тон и язык + +- Безличность нормальна: «выбрано», «представлены», «систематизированы». Без «я» / «мы». +- Ссылки по делу, не в каждом абзаце. Методички «как писать работу» в содержательную часть не тащи. +- Допустима лёгкая неровность: не каждый абзац одной длины, не каждое предложение «отрабатывает ценность». +- Не заканчивай абзацы мета-моралью: «следовательно, целевое решение должно…», «полученная характеристика задаёт основу…», «прикладное значение работы состоит в…» — если это не запрошенная тема. + +## Чего избегать (запахи отчёта ИИ) + +- Текст отвечает на вопрос «что я сделал по пунктам», а не «что это за объект / тема». +- Каждый абзац продаёт себя проверяющему. +- Идеальная симметрия, плотный ряд умных ярлыков подряд, консультантский лоск без живого факта. +- Спор с невидимым рецензентом внутри основного текста. +- Комментарии к собственному методу вместо содержания. + +## Выход + +Соблюдай формат и диалект из базового промпта md2gost. Этот блок меняет только **голос и логику изложения**, не синтаксис разметки. diff --git a/prompts/generate-md.md b/prompts/generate-md.md index fae33f3..3e85017 100644 --- a/prompts/generate-md.md +++ b/prompts/generate-md.md @@ -78,19 +78,61 @@ md2gost читает расширения ниже, нумерует объек **Не** экспортируй UML/Mermaid в PNG вручную и **не** пиши «вставь картинку сюда». -В `.md` оставь fenced-блок — md2gost превратит его в **Рисунок** (PlantUML jar / Kroki; Mermaid — через Kroki). +В `.md` оставь fenced-блок — md2gost превратит его в **Рисунок** (PlantUML jar / Kroki; Mermaid — локально или Kroki; IDEF0 — Pillow; DFD — data-flow-diagram + Graphviz/Kroki). | Оградка | Содержание тела | |---------|-----------------| | `` ```uml `` / `` ```plantuml `` | Полный PlantUML, обычно с `@startuml`…`@enduml` | -| `` ```uml- `` / `` ``` `` | Только макросы схемы (C4, BPMN, usecase…); **без** `@startuml` и **без** `!include` — схема добавит сама | +| `` ```uml- `` / `` ``` `` | Только макросы схемы (C4, usecase…); **без** `@startuml` и **без** `!include` — схема добавит сама | | `` ```mermaid `` / `` ```mmd `` | Синтаксис Mermaid | +| `` ```idef0 `` | IDEF0: боксы, ICOM-стрелки, рамка A-0 | +| `` ```dfd `` / `` ```data-flow-diagram `` | DFD ([pbauermeister/dfd](https://github.com/pbauermeister/dfd)): process/entity/store, `-->` | Перед блоком — `%id Подпись` (и ссылка `@Рисунок:id` в тексте). -**IDEF0** конвертер не рисует — только готовый PNG как обычный Рисунок. +IDEF0 пиши в ````idef0`, не внешним PNG. Пример контекста A-0: -Если в конец этого промпта добавлены блоки «Схема: …» — используй их макросы и оградки. Если блоков нет — для C4/BPMN/usecase всё равно можно писать `` ```uml-c4 ``, `` ```bpmn ``, `` ```usecase `` с типичными макросами; детали пользователь может дописать из GUI. +~~~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 Персонал +``` +~~~ + +DFD пиши в ````dfd` (синтаксис [pbauermeister/dfd](https://github.com/pbauermeister/dfd)), не внешним PNG. Пример контекста: + +~~~markdown +%dfd0 Контекстная DFD + +```dfd +style context + +entity Client Клиент +entity Warehouse Склад +process System Система учёта заявок + +Client --> System заявка +System --> Client статус +System --> Warehouse накладная +``` +~~~ + +Если в конец этого промпта добавлены блоки «Схема: …» — используй их макросы и оградки. Если блоков нет — для C4/usecase всё равно можно писать `` ```uml-c4 ``, `` ```usecase `` с типичными макросами; детали пользователь может дописать из GUI. Пример схемы: @@ -110,7 +152,7 @@ Rel(user, app, "логин") 1. Структура соответствует типу работы (МИРЭА / ПИС / иное — из входа пользователя). 2. У каждого `%id` / title картинки есть `@Тип:id` в тексте. -3. Диаграммы — fence в md, не внешние PNG (кроме IDEF0 и готовых иллюстраций). +3. Диаграммы — fence в md, не внешние PNG (кроме готовых иллюстраций). 4. Нет «рис.» и ссылок `[n]` во введении/заключении. После проверки выдай только markdown. diff --git a/prompts/generate-mirea-report.md b/prompts/generate-mirea-report.md index 8218515..593fd8d 100644 --- a/prompts/generate-mirea-report.md +++ b/prompts/generate-mirea-report.md @@ -102,7 +102,7 @@ ### Диаграмма (PlantUML / Mermaid / схемы) -Перед fenced-блоком с языком `uml`, `plantuml`, `c4`, `uml-c4`, `usecase`, `bpmn` / `uml-bpmn`, `mermaid` / `mmd` и т.п. — `%id Подпись`. Конвертер рисует **Рисунок** (PNG; опционально SVG через `--diagram-format svg`). Опционально `+listing` — ещё и **Листинг** с исходником; `+landscape` — альбомная страница под широкий рисунок/таблицу. Схемы (`c4`, `bpmn`, …) берутся из `md2gost.schemes.json` (создаётся при первом запуске). Mermaid идёт через Kroki (не PlantUML). **IDEF0** не рисуется — вставляйте готовый PNG. +Перед fenced-блоком с языком `uml`, `plantuml`, `c4`, `uml-c4`, `usecase`, `mermaid` / `mmd`, `idef0`, `dfd` / `data-flow-diagram` и т.п. — `%id Подпись`. Конвертер рисует **Рисунок** (PNG; опционально SVG через `--diagram-format svg`). Опционально `+listing` — ещё и **Листинг** с исходником; `+landscape` — альбомная страница под широкий рисунок/таблицу. Схемы (`c4`, `usecase`, …) берутся из `md2gost.schemes.json` (создаётся при первом запуске). Mermaid идёт через Kroki (не PlantUML). **IDEF0** — оградка `` ```idef0 `` (локальный рендер). **DFD** — оградка `` ```dfd `` ([pbauermeister/dfd](https://github.com/pbauermeister/dfd); Graphviz вшитый/PATH или Kroki). ~~~markdown См. @Рисунок:usecase1 и @Листинг:usecase1. @@ -123,20 +123,6 @@ Person(user, "Студент") System(app, "Портал") Rel(user, app, "логин") ``` - -%bpmn1 Процесс заявки - -```bpmn -StartMessage(s, "заявка") -UserTask(t, "Проверить") -XOR(gw, "ок?") -End(e_ok) -End(e_no) -Flow(s, t) -Flow(t, gw) -CondFlow(gw, e_ok, "да") -DefaultFlow(gw, e_no) -``` ~~~ ### Листинг diff --git a/prompts/generate-pis-custom-report.md b/prompts/generate-pis-custom-report.md index f2c5db8..ed6e3e2 100644 --- a/prompts/generate-pis-custom-report.md +++ b/prompts/generate-pis-custom-report.md @@ -62,7 +62,7 @@ User --> (Login) ``` ``` -Языки fence: `uml`, `plantuml`, `mermaid` / `mmd`, `c4` / `uml-c4`, `usecase`, `bpmn` / `uml-bpmn` и другие id из `md2gost.schemes.json`. Mermaid — через Kroki. IDEF0 не поддерживается. Флаг `+listing` в строке `%` добавляет ещё Листинг с исходником; `+landscape` — альбомная страница. +Языки fence: `uml`, `plantuml`, `mermaid` / `mmd`, `idef0`, `dfd` / `data-flow-diagram`, `c4` / `uml-c4`, `usecase` и другие id из `md2gost.schemes.json`. Mermaid — через Kroki. IDEF0 — локально в ````idef0`. DFD — [pbauermeister/dfd](https://github.com/pbauermeister/dfd) в ````dfd` (рендер: Graphviz вшитый/PATH или Kroki). Флаг `+listing` в строке `%` добавляет ещё Листинг с исходником; `+landscape` — альбомная страница. Ссылки: `@Рисунок:usecase1`, `@Листинг:usecase1`. Формула (обычно работа №8): `$$ … $$` с `%eq_id` и ссылкой `@Формула:eq_id`. diff --git a/pyproject.toml b/pyproject.toml index 418224f..4e5f0d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "md2gost" version = "0.1.0" description = "Markdown to DOCX (MIREA TZ / GOST 7.32)" readme = "README.md" -requires-python = ">=3.10" +requires-python = ">=3.11" dependencies = [ "python-docx>=0.8.11,<1", "marko>=2.0.0,<3", @@ -16,6 +16,7 @@ dependencies = [ "lxml>=4.9,<6", "mermaidx>=0.9.0,<1", "playwright>=1.40.0,<2", + "data-flow-diagram>=1.16.0,<2", ] [project.scripts] @@ -23,6 +24,7 @@ md2gost = "md2gost.__main__:main" md2gost-gui = "md2gost.gui:main" md2latex = "md2latex.__main__:main" md2fodt = "md2fodt.__main__:main" +word2md = "word2md.__main__:main" [dependency-groups] dev = [ @@ -39,24 +41,29 @@ requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] -packages = ["md2gost", "md2latex", "md2fodt"] +packages = ["md2gost", "md2latex", "md2fodt", "word2md"] [tool.hatch.build.targets.wheel.force-include] "md2gost/Template.docx" = "md2gost/Template.docx" +"md2gost/TitleTemplate.docx" = "md2gost/TitleTemplate.docx" "md2gost/mml2omml" = "md2gost/mml2omml" "md2gost/diagrams" = "md2gost/diagrams" "latex/mirea" = "latex/mirea" "prompts/generate-md.md" = "md2gost/prompts/generate-md.md" "prompts/generate-mirea-report.md" = "md2gost/prompts/generate-mirea-report.md" "prompts/generate-pis-custom-report.md" = "md2gost/prompts/generate-pis-custom-report.md" +"prompts/emulate-student.md" = "md2gost/prompts/emulate-student.md" "docs/README.md" = "md2gost/docs/README.md" "docs/quickstart.md" = "md2gost/docs/quickstart.md" "docs/markdown.md" = "md2gost/docs/markdown.md" "docs/schemes.md" = "md2gost/docs/schemes.md" +"docs/archimate.md" = "md2gost/docs/archimate.md" "docs/types.md" = "md2gost/docs/types.md" "docs/cli-gui.md" = "md2gost/docs/cli-gui.md" +"docs/title.md" = "md2gost/docs/title.md" "docs/styles.md" = "md2gost/docs/styles.md" "docs/prompts.md" = "md2gost/docs/prompts.md" +"docs/word2md.md" = "md2gost/docs/word2md.md" # Legacy Poetry metadata (optional; prefer uv + [project] above) [tool.poetry] @@ -67,7 +74,7 @@ authors = ["witelokk "] readme = "README.md" [tool.poetry.dependencies] -python = "^3.10" +python = "^3.11" python-docx = "^0.8.11" marko = "^2.0.0" docxcompose = "^1.4.0" @@ -79,6 +86,7 @@ latex2mathml = "^3.76.0" pygments = "^2.16.1" mermaidx = "^0.9.0" playwright = "^1.40.0" +data-flow-diagram = "^1.16.0" [tool.poetry.scripts] md2gost = "md2gost.__main__:main" diff --git a/scripts/_gen_bpmn_sprites.py b/scripts/_gen_bpmn_sprites.py deleted file mode 100644 index b535935..0000000 --- a/scripts/_gen_bpmn_sprites.py +++ /dev/null @@ -1,461 +0,0 @@ -"""One-shot generator for BPMN.puml geometric sprites. Run from repo root.""" -from __future__ import annotations - -import math -from pathlib import Path - -N = 48 -HEX = "0123456789ABCDEF" -SS = 2 - - -def clamp01(v: float) -> float: - return 0.0 if v < 0 else 1.0 if v > 1 else v - - -def mix(grid: list[list[float]], x: int, y: int, v: float) -> None: - if 0 <= x < N and 0 <= y < N: - grid[y][x] = max(grid[y][x], clamp01(v)) - - -def ring(d: float, r: float, hw: float) -> float: - return clamp01(1.0 - abs(d - r) / hw) - - -def disk(d: float, r: float, hw: float = 0.65) -> float: - if d <= r - hw: - return 1.0 - if d >= r + hw: - return 0.0 - return clamp01((r + hw - d) / (2 * hw)) - - -def line_cov(px: float, py: float, x0: float, y0: float, x1: float, y1: float, hw: float) -> float: - dx, dy = x1 - x0, y1 - y0 - len2 = dx * dx + dy * dy - if len2 < 1e-6: - return disk(math.hypot(px - x0, py - y0), 0, hw) - t = clamp01(((px - x0) * dx + (py - y0) * dy) / len2) - qx, qy = x0 + t * dx, y0 + t * dy - return disk(math.hypot(px - qx, py - qy), 0, hw) - - -def diamond_m(px: float, py: float, cx: float, cy: float) -> float: - return abs(px - cx) + abs(py - cy) - - -def sample_event( - px: float, - py: float, - *, - kind: str, - dashed: bool, -) -> float: - cx = cy = (N - 1) / 2 - d = math.hypot(px - cx, py - cy) - v = 0.0 - if kind == "start": - v = max(v, ring(d, 18.2, 1.15)) - elif kind == "inter": - v = max(v, ring(d, 18.4, 1.05)) - v = max(v, ring(d, 14.6, 1.05)) - elif kind == "end": - v = max(v, ring(d, 17.6, 2.55)) - elif kind == "endfill": - v = max(v, ring(d, 17.6, 2.55)) - v = max(v, disk(d, 7.2, 0.7)) - if dashed and v > 0: - ang = (math.atan2(py - cy, px - cx) + math.pi) / (2 * math.pi) - if int(ang * 18) % 2 == 1: - v *= 0.08 - return v - - -def icon_msg(px: float, py: float, cx: float, cy: float, filled: bool) -> float: - # envelope 16x10 around center - x0, y0, x1, y1 = cx - 8, cy - 5, cx + 8, cy + 5 - v = 0.0 - # outer rect - inside = (x0 + 0.8 <= px <= x1 - 0.8) and (y0 + 0.8 <= py <= y1 - 0.8) - on_border = ( - (abs(px - x0) < 1.1 or abs(px - x1) < 1.1) and (y0 - 0.4 <= py <= y1 + 0.4) - ) or ( - (abs(py - y0) < 1.1 or abs(py - y1) < 1.1) and (x0 - 0.4 <= px <= x1 + 0.4) - ) - if filled: - if inside or on_border: - v = 1.0 - # inverted flap (white cut) approximated by not drawing center of flap - v = max(v, line_cov(px, py, x0, y0, cx, cy + 1.5, 0.9)) - v = max(v, line_cov(px, py, x1, y0, cx, cy + 1.5, 0.9)) - return v - v = max(v, 1.0 if on_border else 0.0) - v = max(v, line_cov(px, py, x0, y0, cx, cy + 1.8, 0.95)) - v = max(v, line_cov(px, py, x1, y0, cx, cy + 1.8, 0.95)) - return v - - -def icon_timer(px: float, py: float, cx: float, cy: float) -> float: - d = math.hypot(px - cx, py - cy) - v = ring(d, 7.2, 1.0) - v = max(v, line_cov(px, py, cx, cy, cx, cy - 5.2, 0.9)) - v = max(v, line_cov(px, py, cx, cy, cx + 3.6, cy, 0.9)) - return v - - -def icon_error(px: float, py: float, cx: float, cy: float) -> float: - pts = [ - (cx + 1, cy - 8), - (cx - 2, cy - 1), - (cx + 3, cy - 1), - (cx - 1, cy + 8), - (cx + 0.5, cy + 1.5), - (cx - 3.5, cy + 1.5), - ] - v = 0.0 - for a, b in zip(pts, pts[1:] + pts[:1]): - v = max(v, line_cov(px, py, a[0], a[1], b[0], b[1], 1.0)) - return v - - -def icon_signal(px: float, py: float, cx: float, cy: float, filled: bool) -> float: - a, b, c = (cx, cy - 7.5), (cx - 7.2, cy + 6), (cx + 7.2, cy + 6) - v = 0.0 - v = max(v, line_cov(px, py, *a, *b, 1.05)) - v = max(v, line_cov(px, py, *b, *c, 1.05)) - v = max(v, line_cov(px, py, *c, *a, 1.05)) - if filled: - # barycentric fill - def area(p, q, r): - return (q[0] - p[0]) * (r[1] - p[1]) - (q[1] - p[1]) * (r[0] - p[0]) - - p = (px, py) - a0 = area(a, b, c) - if abs(a0) > 0.1: - w1 = area(p, b, c) / a0 - w2 = area(a, p, c) / a0 - w3 = area(a, b, p) / a0 - if min(w1, w2, w3) >= -0.02: - v = max(v, 1.0) - return v - - -def icon_esc(px: float, py: float, cx: float, cy: float) -> float: - v = 0.0 - v = max(v, line_cov(px, py, cx, cy + 7, cx, cy - 2, 1.1)) - v = max(v, line_cov(px, py, cx, cy - 2, cx - 6, cy + 4, 1.05)) - v = max(v, line_cov(px, py, cx, cy - 2, cx + 6, cy + 4, 1.05)) - v = max(v, line_cov(px, py, cx - 6, cy + 4, cx + 6, cy + 4, 1.0)) - return v - - -def icon_link(px: float, py: float, cx: float, cy: float) -> float: - v = line_cov(px, py, cx - 8, cy, cx + 5, cy, 1.1) - v = max(v, line_cov(px, py, cx + 5, cy, cx + 1, cy - 4, 1.05)) - v = max(v, line_cov(px, py, cx + 5, cy, cx + 1, cy + 4, 1.05)) - return v - - -def icon_comp(px: float, py: float, cx: float, cy: float) -> float: - v = 0.0 - for ox in (-4.2, 3.2): - a, b, c = (cx + ox + 4, cy), (cx + ox - 3.5, cy - 6), (cx + ox - 3.5, cy + 6) - v = max(v, line_cov(px, py, *a, *b, 1.0)) - v = max(v, line_cov(px, py, *a, *c, 1.0)) - v = max(v, line_cov(px, py, *b, *c, 1.0)) - return v - - -def icon_cond(px: float, py: float, cx: float, cy: float) -> float: - x0, y0, x1, y1 = cx - 6, cy - 8, cx + 6, cy + 8 - v = 0.0 - on = ( - (abs(px - x0) < 1.0 or abs(px - x1) < 1.0) and (y0 <= py <= y1) - ) or ( - (abs(py - y0) < 1.0 or abs(py - y1) < 1.0) and (x0 <= px <= x1) - ) - if on: - v = 1.0 - for yy in (cy - 3, cy, cy + 3): - v = max(v, line_cov(px, py, cx - 3.5, yy, cx + 3.5, yy, 0.8)) - return v - - -def icon_multi(px: float, py: float, cx: float, cy: float) -> float: - r = 7.4 - pts = [] - for i in range(5): - a = -math.pi / 2 + i * 2 * math.pi / 5 - pts.append((cx + r * math.cos(a), cy + r * math.sin(a))) - v = 0.0 - for a, b in zip(pts, pts[1:] + pts[:1]): - v = max(v, line_cov(px, py, *a, *b, 1.0)) - return v - - -def icon_par_multi(px: float, py: float, cx: float, cy: float) -> float: - v = ring(math.hypot(px - cx, py - cy), 7.4, 1.0) - v = max(v, line_cov(px, py, cx, cy - 5, cx, cy + 5, 1.05)) - v = max(v, line_cov(px, py, cx - 5, cy, cx + 5, cy, 1.05)) - return v - - -def icon_cancel(px: float, py: float, cx: float, cy: float) -> float: - v = line_cov(px, py, cx - 6, cy - 6, cx + 6, cy + 6, 1.15) - v = max(v, line_cov(px, py, cx + 6, cy - 6, cx - 6, cy + 6, 1.15)) - return v - - -def icon_user(px: float, py: float, cx: float, cy: float) -> float: - v = disk(math.hypot(px - cx, py - (cy - 3.2)), 3.1, 0.7) - v = max(v, ring(math.hypot(px - cx, py - (cy - 3.2)), 3.1, 0.9)) - # shoulders - v = max(v, line_cov(px, py, cx - 6, cy + 7, cx + 6, cy + 7, 1.1)) - v = max(v, line_cov(px, py, cx - 6, cy + 7, cx - 4, cy + 1.5, 1.0)) - v = max(v, line_cov(px, py, cx + 6, cy + 7, cx + 4, cy + 1.5, 1.0)) - return v - - -def icon_service(px: float, py: float, cx: float, cy: float) -> float: - v = ring(math.hypot(px - cx, py - cy), 4.2, 1.15) - for i in range(8): - a = i * math.pi / 4 - v = max( - v, - line_cov( - px, py, - cx + 3.2 * math.cos(a), cy + 3.2 * math.sin(a), - cx + 7.4 * math.cos(a), cy + 7.4 * math.sin(a), - 1.05, - ), - ) - return v - - -def icon_script(px: float, py: float, cx: float, cy: float) -> float: - return icon_cond(px, py, cx, cy) - - -def icon_manual(px: float, py: float, cx: float, cy: float) -> float: - v = line_cov(px, py, cx - 7, cy + 2, cx + 7, cy + 2, 1.2) - v = max(v, line_cov(px, py, cx - 7, cy + 2, cx - 5, cy - 4, 1.0)) - v = max(v, line_cov(px, py, cx - 2, cy + 2, cx - 1, cy - 6, 1.0)) - v = max(v, line_cov(px, py, cx + 2, cy + 2, cx + 3, cy - 5.5, 1.0)) - v = max(v, line_cov(px, py, cx + 6, cy + 2, cx + 7, cy - 3, 1.0)) - return v - - -def icon_rule(px: float, py: float, cx: float, cy: float) -> float: - v = 0.0 - for yy in (cy - 6, cy, cy + 6): - v = max(v, line_cov(px, py, cx - 7, yy, cx + 7, yy, 0.95)) - for xx in (cx - 7, cx, cx + 7): - v = max(v, line_cov(px, py, xx, cy - 6, xx, cy + 6, 0.95)) - return v - - -def icon_plusbox(px: float, py: float, cx: float, cy: float) -> float: - x0, y0, x1, y1 = cx - 7, cy - 7, cx + 7, cy + 7 - v = 0.0 - on = ( - (abs(px - x0) < 1.05 or abs(px - x1) < 1.05) and (y0 <= py <= y1) - ) or ( - (abs(py - y0) < 1.05 or abs(py - y1) < 1.05) and (x0 <= px <= x1) - ) - if on: - v = 1.0 - v = max(v, line_cov(px, py, cx, cy - 4.5, cx, cy + 4.5, 1.05)) - v = max(v, line_cov(px, py, cx - 4.5, cy, cx + 4.5, cy, 1.05)) - return v - - -ICONS = { - "": None, - "msg": lambda px, py, cx, cy: icon_msg(px, py, cx, cy, False), - "msgf": lambda px, py, cx, cy: icon_msg(px, py, cx, cy, True), - "timer": icon_timer, - "error": icon_error, - "signal": lambda px, py, cx, cy: icon_signal(px, py, cx, cy, False), - "signalf": lambda px, py, cx, cy: icon_signal(px, py, cx, cy, True), - "esc": icon_esc, - "link": icon_link, - "comp": icon_comp, - "cond": icon_cond, - "multi": icon_multi, - "par": icon_par_multi, - "cancel": icon_cancel, - "user": icon_user, - "service": icon_service, - "script": icon_script, - "manual": icon_manual, - "rule": icon_rule, - "plusbox": icon_plusbox, -} - - -def raster(draw) -> list[list[float]]: - acc = [[0.0] * N for _ in range(N)] - big = N * SS - for y in range(big): - for x in range(big): - v = draw((x + 0.5) / SS, (y + 0.5) / SS) - acc[y // SS][x // SS] += v - s = float(SS * SS) - return [[acc[y][x] / s for x in range(N)] for y in range(N)] - - -def to_sprite(name: str, grid: list[list[float]]) -> str: - lines = [f"sprite ${name} [{N}x{N}/16] {{"] - for row in grid: - lines.append("".join(HEX[min(15, int(round(v * 15)))] for v in row)) - lines.append("}") - return "\n".join(lines) - - -def event_grid(kind: str, icon: str, dashed: bool = False) -> list[list[float]]: - cx = cy = (N - 1) / 2.0 - fn = ICONS[icon] - - def draw(px, py): - v = sample_event(px, py, kind=kind, dashed=dashed) - if fn is not None: - v = max(v, fn(px, py, cx, cy)) - return v - - return raster(draw) - - -def gateway_grid(mark: str) -> list[list[float]]: - cx = cy = (N - 1) / 2.0 - r = 21.0 - - def draw(px, py): - m = diamond_m(px, py, cx, cy) - v = ring(m, r, 1.25) - if mark == "x": - v = max(v, line_cov(px, py, cx - 8, cy - 8, cx + 8, cy + 8, 1.25)) - v = max(v, line_cov(px, py, cx + 8, cy - 8, cx - 8, cy + 8, 1.25)) - elif mark == "+": - v = max(v, line_cov(px, py, cx, cy - 9, cx, cy + 9, 1.3)) - v = max(v, line_cov(px, py, cx - 9, cy, cx + 9, cy, 1.3)) - elif mark == "o": - v = max(v, ring(math.hypot(px - cx, py - cy), 7.0, 1.15)) - elif mark == "*": - for i in range(4): - a = i * math.pi / 4 - v = max( - v, - line_cov( - px, py, - cx - 7 * math.cos(a), cy - 7 * math.sin(a), - cx + 7 * math.cos(a), cy + 7 * math.sin(a), - 1.05, - ), - ) - elif mark == "ev": - v = max(v, icon_multi(px, py, cx, cy)) - v = max(v, ring(math.hypot(px - cx, py - cy), 10.5, 0.95)) - elif mark == "pev": - v = max(v, icon_par_multi(px, py, cx, cy)) - return v - - return raster(draw) - - -EVENTS = [ - ("evStart", "start", "", False), - ("evStartMsg", "start", "msg", False), - ("evStartTimer", "start", "timer", False), - ("evStartSignal", "start", "signal", False), - ("evStartCond", "start", "cond", False), - ("evStartError", "start", "error", False), - ("evStartEsc", "start", "esc", False), - ("evStartMulti", "start", "multi", False), - ("evStartPar", "start", "par", False), - ("evStartNI", "start", "", True), - ("evStartMsgNI", "start", "msg", True), - ("evStartTimerNI", "start", "timer", True), - ("evStartSignalNI", "start", "signal", True), - ("evInter", "inter", "", False), - ("evCatchMsg", "inter", "msg", False), - ("evCatchTimer", "inter", "timer", False), - ("evCatchSignal", "inter", "signal", False), - ("evCatchError", "inter", "error", False), - ("evCatchEsc", "inter", "esc", False), - ("evCatchCond", "inter", "cond", False), - ("evCatchLink", "inter", "link", False), - ("evCatchComp", "inter", "comp", False), - ("evCatchCancel", "inter", "cancel", False), - ("evCatchMulti", "inter", "multi", False), - ("evCatchPar", "inter", "par", False), - ("evThrowMsg", "inter", "msgf", False), - ("evThrowSignal", "inter", "signalf", False), - ("evThrowEsc", "inter", "esc", False), - ("evThrowLink", "inter", "link", False), - ("evThrowComp", "inter", "comp", False), - ("evThrowMulti", "inter", "multi", False), - ("evEnd", "end", "", False), - ("evEndMsg", "end", "msgf", False), - ("evEndError", "end", "error", False), - ("evEndEsc", "end", "esc", False), - ("evEndCancel", "end", "cancel", False), - ("evEndComp", "end", "comp", False), - ("evEndSignal", "end", "signalf", False), - ("evEndMulti", "end", "multi", False), - ("evTerminate", "endfill", "", False), - ("evBoundMsg", "inter", "msg", False), - ("evBoundTimer", "inter", "timer", False), - ("evBoundError", "inter", "error", False), - ("evBoundEsc", "inter", "esc", False), - ("evBoundSignal", "inter", "signal", False), - ("evBoundCancel", "inter", "cancel", False), - ("evBoundComp", "inter", "comp", False), - ("evBoundCond", "inter", "cond", False), - ("evBoundMsgNI", "inter", "msg", True), - ("evBoundTimerNI", "inter", "timer", True), - ("evBoundEscNI", "inter", "esc", True), - ("evBoundSignalNI", "inter", "signal", True), - ("evBoundCondNI", "inter", "cond", True), -] - -GATES = [ - ("gwXor", "x"), - ("gwAnd", "+"), - ("gwOr", "o"), - ("gwComplex", "*"), - ("gwEvent", "ev"), - ("gwParEvent", "pev"), -] - -TASK_ICONS = ["user", "service", "script", "manual", "rule", "plusbox", "msg", "msgf"] - - -def main() -> None: - chunks = ["' auto-generated geometric sprites (BPMN 2.0 notation)"] - for name, kind, icon, dashed in EVENTS: - chunks.append(to_sprite(name, event_grid(kind, icon, dashed))) - chunks.append("") - for name, mark in GATES: - chunks.append(to_sprite(name, gateway_grid(mark))) - chunks.append("") - cx = cy = (N - 1) / 2.0 - for icon in TASK_ICONS: - fn = ICONS[icon] - grid = raster(lambda px, py, fn=fn: fn(px, py, cx, cy)) - chunks.append(to_sprite("ic" + icon.capitalize() if icon != "msgf" else "icMsgf", grid)) - chunks.append("") - # plusbox already as icPlusbox; loop/adhoc/mi as simple extra - def loop_draw(px, py): - v = ring(math.hypot(px - cx, py - (cy - 1)), 8.5, 1.1) - v = max(v, line_cov(px, py, cx + 6, cy + 2, cx + 10, cy + 6, 1.05)) - v = max(v, line_cov(px, py, cx + 6, cy + 2, cx + 2, cy + 6, 1.05)) - return v - - chunks.append(to_sprite("icLoop", raster(loop_draw))) - chunks.append("") - Path("_bpmn_sprites.puml").write_text("\n".join(chunks), encoding="utf-8") - print("wrote", len(chunks), "chunks") - - -if __name__ == "__main__": - main() diff --git a/scripts/fetch_graphviz.py b/scripts/fetch_graphviz.py new file mode 100644 index 0000000..9748c18 --- /dev/null +++ b/scripts/fetch_graphviz.py @@ -0,0 +1,44 @@ +"""Download official Graphviz win64 portable into md2gost/vendor/graphviz (for exe builds).""" + +from __future__ import annotations + +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +from md2gost.dfd import ( # noqa: E402 + GRAPHVIZ_VERSION, + GRAPHVIZ_ZIP_URL, + fetch_graphviz, + resolve_graphviz_engine, + strip_graphviz_tcl_libs, + vendor_graphviz_root, +) + + +def main() -> int: + dest = vendor_graphviz_root() + print(f"Graphviz {GRAPHVIZ_VERSION}: {GRAPHVIZ_ZIP_URL}") + print(f"Куда: {dest}") + if resolve_graphviz_engine(False) and (dest / "bin").is_dir(): + n = strip_graphviz_tcl_libs(dest) + hit = resolve_graphviz_engine(False) + extra = f", снял Tcl/Tk DLL Graphviz: {n}" if n else "" + print(f"Уже есть: {hit}{extra}") + return 0 + if fetch_graphviz(dest): + hit = resolve_graphviz_engine(False) + print(f"Готово: {hit or dest}") + return 0 + print( + "Не удалось скачать Graphviz " + "(exe соберётся; DFD пойдёт через kroki.io или системный PATH)." + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/pyi_bundle.py b/scripts/pyi_bundle.py new file mode 100644 index 0000000..739c985 --- /dev/null +++ b/scripts/pyi_bundle.py @@ -0,0 +1,111 @@ +"""PyInstaller freeze helpers: pin CPython Tcl/Tk, drop Graphviz's copy.""" + +from __future__ import annotations + +import os +import sys +from pathlib import Path + + +def _python_dll_dir() -> Path | None: + candidates = [ + Path(sys.base_prefix) / "DLLs", + Path(sys.prefix) / "DLLs", + Path(sys.executable).resolve().parent / "DLLs", + Path(sys.executable).resolve().parent.parent / "DLLs", + ] + for folder in candidates: + if (folder / "tcl86t.dll").is_file() or (folder / "tk86t.dll").is_file(): + return folder + return None + + +def prepare_tcltk_env() -> None: + """Make Analysis / isolated hook subprocesses see this interpreter's Tcl.""" + dll_dir = _python_dll_dir() + prefix = Path(sys.base_prefix) + tcl_lib = prefix / "tcl" / "tcl8.6" + tk_lib = prefix / "tcl" / "tk8.6" + if not tcl_lib.is_dir() and dll_dir is not None: + tcl_lib = dll_dir.parent / "tcl" / "tcl8.6" + tk_lib = dll_dir.parent / "tcl" / "tk8.6" + if tcl_lib.is_dir(): + os.environ["TCL_LIBRARY"] = str(tcl_lib) + if tk_lib.is_dir(): + os.environ["TK_LIBRARY"] = str(tk_lib) + if dll_dir is not None: + os.environ["PATH"] = str(dll_dir) + os.pathsep + os.environ.get("PATH", "") + + +def collect_tkinterdnd2(): + datas: list = [] + hidden: list = ["tkinterdnd2", "tkinterdnd2.TkinterDnD"] + try: + from PyInstaller.utils.hooks import collect_data_files, collect_submodules + + datas += collect_data_files("tkinterdnd2") + hidden = list(collect_submodules("tkinterdnd2")) or hidden + except Exception: + pass + return datas, hidden + + +def _toc_name(item) -> str: + dest = str(item[0]).replace("\\", "/") + return os.path.basename(dest).lower() + + +def _toc_blob(item) -> str: + dest = str(item[0]).replace("\\", "/").lower() + src = str(item[1]).replace("\\", "/").lower() if len(item) > 1 else "" + return dest + " " + src + + +def _graphviz_tcl_names() -> set[str]: + try: + from md2gost.dfd import GRAPHVIZ_TCL_DLL_NAMES + + return {n.lower() for n in GRAPHVIZ_TCL_DLL_NAMES} + except Exception: + return { + "tcl86t.dll", + "tk86t.dll", + "tcl86.dll", + "tk86.dll", + "tcldot.dll", + "tcldot_builtin.dll", + "tclplan.dll", + "gdtclft.dll", + } + + +def _is_graphviz_tcl(item) -> bool: + if _toc_name(item) not in _graphviz_tcl_names(): + return False + return "graphviz" in _toc_blob(item) + + +def _wrap_toc(items): + try: + from PyInstaller.building.datastruct import TOC + + return TOC(items) + except Exception: + return items + + +def sanitize_analysis(a): + """Drop Graphviz Tcl/Tk DLLs and force this CPython's tcl86t/tk86t into the exe.""" + a.datas = _wrap_toc([x for x in a.datas if not _is_graphviz_tcl(x)]) + binaries = [x for x in a.binaries if not _is_graphviz_tcl(x)] + dll_dir = _python_dll_dir() + pin = ("tcl86t.dll", "tk86t.dll", "zlib1.dll") + if dll_dir is not None: + pin_lower = {n.lower() for n in pin} + binaries = [x for x in binaries if _toc_name(x) not in pin_lower] + for name in pin: + path = dll_dir / name + if path.is_file(): + binaries.append((name, str(path), "BINARY")) + a.binaries = _wrap_toc(binaries) + return a diff --git a/scripts/pyi_rth_tcltk.py b/scripts/pyi_rth_tcltk.py new file mode 100644 index 0000000..3e54c0d --- /dev/null +++ b/scripts/pyi_rth_tcltk.py @@ -0,0 +1,33 @@ +# Runtime hook: load this CPython's Tcl/Tk before Graphviz's tcl86t.dll (8.6.10) +# can shadow them. PyInstaller then points TCL_LIBRARY at bundled _tcl_data (8.6.12). +def _pyi_rthook(): + import os + import sys + + if not getattr(sys, "frozen", False): + return + base = getattr(sys, "_MEIPASS", None) or os.path.dirname(sys.executable) + os.environ["PATH"] = base + os.pathsep + os.environ.get("PATH", "") + try: + os.add_dll_directory(base) + except (AttributeError, OSError): + pass + tcldir = os.path.join(base, "_tcl_data") + tkdir = os.path.join(base, "_tk_data") + if os.path.isdir(tcldir): + os.environ["TCL_LIBRARY"] = tcldir + if os.path.isdir(tkdir): + os.environ["TK_LIBRARY"] = tkdir + try: + import ctypes + + for name in ("tcl86t.dll", "tk86t.dll"): + path = os.path.join(base, name) + if os.path.isfile(path): + ctypes.WinDLL(path) + except Exception: + pass + + +_pyi_rthook() +del _pyi_rthook diff --git a/tests/_idef0_preview.png b/tests/_idef0_preview.png new file mode 100644 index 0000000000000000000000000000000000000000..4c37536555ec3ebdab1891b5ae0afc104a6eb7a1 GIT binary patch literal 71059 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�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 literal 0 HcmV?d00001 diff --git a/tests/_idef0_preview.svg b/tests/_idef0_preview.svg new file mode 100644 index 0000000..ddde844 --- /dev/null +++ b/tests/_idef0_preview.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + +USED AT: +AUTHOR: +Кинзябулатов Рамиль +PROJECT: +Разработка универсальной модели торгового предприятия +NOTES: +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +DATE: +08.02.2022 +REV: +31.03.2022 + +WORKING + +DRAFT + +RECOMMENDED + +PUBLICATION +READER + +DATE +CONTEXT: +TOP + + +NODE: +A-0 +TITLE: +Распорядиться товаром +NUMBER: +1 + +1 + + + + + + + + + +Спрос + +Нормативная +документация + +Товар + +Персонал + + + +Распорядиться товаром +A0 + diff --git a/tests/_idef0_preview_a0.png b/tests/_idef0_preview_a0.png new file mode 100644 index 0000000000000000000000000000000000000000..c1b52fc8f861f1275892abeabc63f87bc7b76371 GIT binary patch literal 116528 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 lih + + + + + + + + + + + +USED AT: +AUTHOR: +Кинзябулатов Рамиль +PROJECT: +Разработка универсальной модели торгового предприятия +NOTES: +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +DATE: +08.02.2022 +REV: +31.03.2022 + +WORKING + +DRAFT + +RECOMMENDED + +PUBLICATION +READER + +DATE +CONTEXT: + + +A-0 + + +NODE: +A0 +TITLE: +Распорядиться товаром +NUMBER: +2 + +2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +I1 + +I2 + +C1 + +C2 + +C3 + +O1 + +O2 + +M1 + +M2 + +Спрос + +Предложения +поставщиков + +Нормативная +документация + +Ассортиментная +политика + +Условия +поставки + +план закупок + +график +поставок + +товар к отгрузке + +Товар + +Акт отгрузки + +Персонал + +Склад + + + +Принять и оценить спрос +A1 + + + +Закупить и хранить +A2 + + + +Отгрузить товар +A3 + diff --git a/tests/test_dfd.py b/tests/test_dfd.py new file mode 100644 index 0000000..342cf04 --- /dev/null +++ b/tests/test_dfd.py @@ -0,0 +1,194 @@ +"""DFD via pbauermeister/data-flow-diagram (Graphviz / Kroki).""" + +from __future__ import annotations + +from pathlib import Path +from unittest.mock import patch + +import pytest + +from md2gost.diagram_renderer import ( + DIAGRAM_LANGS, + configure_diagrams, + is_diagram_lang, + prepare_source, + render_diagram, +) +from md2gost.dfd import ( + SAMPLE_CONTEXT, + SAMPLE_LEVEL0, + DfdParseError, + _compile_dot, + write_dfd_outputs, +) +from md2gost.extended_markdown import markdown +from md2gost.extended_markdown.caption import Caption as CapEl +from md2gost.renderable.caption import CaptionInfo +from md2gost.renderable.diagram import DiagramFigure +from md2gost.renderable_factory import RenderableFactory + + +def test_is_diagram_lang_dfd(): + assert is_diagram_lang("dfd") + assert is_diagram_lang("uml-dfd") + assert is_diagram_lang("data-flow-diagram") + assert is_diagram_lang("yourdon") + assert is_diagram_lang("uml-yourdon") + assert "dfd" in DIAGRAM_LANGS + + +def test_prepare_dfd_stays_native(): + src, dtype = prepare_source("dfd", SAMPLE_CONTEXT) + assert dtype == "dfd" + assert "@startuml" not in src.lower() + assert "process" in src + assert "System" in src + for lang in ("uml-dfd", "data-flow-diagram", "yourdon"): + _, dtype2 = prepare_source(lang, SAMPLE_LEVEL0) + assert dtype2 == "dfd" + + +def test_compile_dot_empty_raises(): + with pytest.raises(DfdParseError, match="пустой"): + _compile_dot(" \n\n ") + + +def test_compile_dot_sample(monkeypatch): + """When the package is importable, real compile; else accept mock.""" + try: + dot, is_ctx = _compile_dot(SAMPLE_CONTEXT) + except DfdParseError as exc: + if "data-flow-diagram не найден" in str(exc) or "не найден" in str(exc): + pytest.skip("data-flow-diagram not installed for this Python") + raise + assert "digraph" in dot.lower() or "graph" in dot.lower() + assert is_ctx is True + + +def test_strip_graphviz_tcl_libs(tmp_path): + from md2gost.dfd import GRAPHVIZ_TCL_DLL_NAMES, strip_graphviz_tcl_libs + + bin_dir = tmp_path / "bin" + bin_dir.mkdir() + (bin_dir / "dot.exe").write_bytes(b"mz") + (bin_dir / "tcl86t.dll").write_bytes(b"tcl") + (bin_dir / "gdtclft.dll").write_bytes(b"gd") + (bin_dir / "cgraph.dll").write_bytes(b"keep") + tcl_mod = tmp_path / "lib" / "graphviz" / "tcl" + tcl_mod.mkdir(parents=True) + (tcl_mod / "pkgIndex.tcl").write_text("x", encoding="utf-8") + + removed = strip_graphviz_tcl_libs(tmp_path) + assert removed >= 3 + assert not (bin_dir / "tcl86t.dll").exists() + assert not (bin_dir / "gdtclft.dll").exists() + assert (bin_dir / "dot.exe").exists() + assert (bin_dir / "cgraph.dll").exists() + assert not tcl_mod.exists() + for name in GRAPHVIZ_TCL_DLL_NAMES: + assert not (bin_dir / name).exists() + + +def test_write_dfd_outputs_uses_local_or_kroki(tmp_path): + png = tmp_path / "a.png" + svg = tmp_path / "a.svg" + fake_dot = "digraph G { a -> b; }\n" + + def fake_local(dot, out_path, fmt, *, context): + Path(out_path).write_bytes( + b"\x89PNG\r\n\x1a\n" if fmt == "png" else b"" + ) + if fmt == "png": + # minimal valid-enough PNG header for startswith checks + Path(out_path).write_bytes( + b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR" + + b"\x00" * 20 + ) + else: + Path(out_path).write_text( + "Клиент", + encoding="utf-8", + ) + return True + + with patch("md2gost.dfd._compile_dot", return_value=(fake_dot, False)), \ + patch("md2gost.dfd._render_dot_local", side_effect=fake_local), \ + patch("md2gost.dfd._render_dot_kroki", return_value=False): + assert write_dfd_outputs(SAMPLE_LEVEL0, png, svg, scale=1) + assert png.read_bytes().startswith(b"\x89PNG") + assert "Клиент" in svg.read_text(encoding="utf-8") + + +def test_write_dfd_outputs_falls_back_to_kroki(tmp_path): + png = tmp_path / "a.png" + + def fake_kroki(dot, out_path, fmt): + Path(out_path).write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 32) + return True + + with patch("md2gost.dfd._compile_dot", return_value=("digraph G {}", False)), \ + patch("md2gost.dfd._render_dot_local", return_value=False), \ + patch("md2gost.dfd._render_dot_kroki", side_effect=fake_kroki): + assert write_dfd_outputs(SAMPLE_CONTEXT, png, None, scale=1) + assert png.read_bytes().startswith(b"\x89PNG") + + +def test_write_dfd_outputs_raises_when_no_renderer(tmp_path): + with patch("md2gost.dfd._compile_dot", return_value=("digraph G {}", False)), \ + patch("md2gost.dfd._render_dot_local", return_value=False), \ + patch("md2gost.dfd._render_dot_kroki", return_value=False): + with pytest.raises(DfdParseError, match="Graphviz|Kroki"): + write_dfd_outputs(SAMPLE_CONTEXT, tmp_path / "x.png", None) + + +def test_render_diagram_dfd_skips_jar(tmp_path): + calls = {"jar": 0} + + def fake_jar(*args, **kwargs): + calls["jar"] += 1 + return False + + def fake_write(source, out_png, out_svg, *, scale=2.0): + Path(out_png).write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 32) + return True + + configure_diagrams(fallback="off", cache_dir=str(tmp_path), diagram_scale=1) + with patch("md2gost.diagram_renderer._render_plantuml_jar", side_effect=fake_jar), \ + patch("md2gost.dfd.write_dfd_outputs", side_effect=fake_write): + result = render_diagram( + "dfd", SAMPLE_CONTEXT, cache_dir=str(tmp_path), diagram_scale=1, + ) + assert calls["jar"] == 0 + assert Path(result.png_path).read_bytes().startswith(b"\x89PNG") + + +def test_render_diagram_dfd_bad_source(tmp_path): + configure_diagrams(fallback="off", cache_dir=str(tmp_path)) + with patch("md2gost.dfd._compile_dot", side_effect=DfdParseError("пустой DFD")): + with pytest.raises(RuntimeError, match="пустой|DFD"): + render_diagram("dfd", " ", cache_dir=str(tmp_path)) + + +def test_factory_routes_dfd_to_diagram(): + from docx import Document + from md2gost import package_dir + import os + + md = "%ctx Контекст DFD\n\n```dfd\n" + SAMPLE_CONTEXT + "```\n" + parsed = markdown.parse(md) + doc = Document(os.path.join(package_dir(), "Template.docx")) + factory = RenderableFactory(doc._body) + caption = None + renderables = [] + for el in parsed.children: + from marko.block import BlankLine + if isinstance(el, BlankLine): + continue + if isinstance(el, CapEl): + caption = CaptionInfo(el.unique_name, el.text, el.with_listing) + continue + renderables.append(factory.create(el, caption)) + caption = None + assert len(renderables) == 1 + assert isinstance(renderables[0], DiagramFigure) + assert renderables[0]._lang == "dfd" diff --git a/tests/test_diagram_renderer.py b/tests/test_diagram_renderer.py index 9c17e59..3d5ac15 100644 --- a/tests/test_diagram_renderer.py +++ b/tests/test_diagram_renderer.py @@ -5,6 +5,7 @@ from __future__ import annotations from pathlib import Path from unittest.mock import patch +import json import pytest from md2gost.diagram_includes import ( @@ -25,8 +26,12 @@ from md2gost.diagram_renderer import ( from md2gost.diagram_schemes import ( DiagramScheme, apply_scheme, + bundled_schemes_path, + claim_user_scheme_if_diverged, configure_schemes, ensure_user_schemes, + load_schemes_from_path, + migrate_user_schemes, prepare_with_schemes, scheme_id_from_lang, user_schemes_path, @@ -83,35 +88,21 @@ def test_unknown_scheme_raises(tmp_path, monkeypatch): prepare_with_schemes("uml-nosuch", "A -> B", base=tmp_path) -def test_bpmn_scheme_prepare(tmp_path, monkeypatch): - monkeypatch.chdir(tmp_path) - ensure_user_schemes(tmp_path) - configure_schemes(base=tmp_path) - assert is_diagram_lang("bpmn") - assert is_diagram_lang("uml-bpmn") - src, dtype = prepare_with_schemes("bpmn", "Start(s)\nEnd(e)\nFlow(s, e)", base=tmp_path) - assert dtype == "plantuml" - assert "Start(s)" in src - assert "!include" in src - assert "BPMN.puml" in src.replace("\\", "/") - assert src.lower().count("@startuml") == 1 - - -def test_bundled_bpmn_survives_user_file_without_it(tmp_path, monkeypatch): +def test_bundled_c4_survives_user_file_without_it(tmp_path, monkeypatch): monkeypatch.chdir(tmp_path) path = ensure_user_schemes(tmp_path) path.write_text('{"mine":{"title":"x","prefix":"","postfix":""}}\n', encoding="utf-8") configure_schemes(base=tmp_path) assert is_diagram_lang("mine") - assert is_diagram_lang("bpmn") + assert is_diagram_lang("c4") def test_scheme_id_from_lang(): assert scheme_id_from_lang("uml") is None assert scheme_id_from_lang("uml-c4") == "c4" assert scheme_id_from_lang("c4") == "c4" - assert scheme_id_from_lang("bpmn") == "bpmn" - assert scheme_id_from_lang("uml-bpmn") == "bpmn" + assert scheme_id_from_lang("usecase") == "usecase" + assert scheme_id_from_lang("uml-usecase") == "usecase" def test_ensure_user_schemes_once(tmp_path): @@ -121,10 +112,129 @@ def test_ensure_user_schemes_once(tmp_path): path.write_text('{"mine":{"title":"x","prefix":"","postfix":""}}\n', encoding="utf-8") again = ensure_user_schemes(tmp_path) assert again == path - assert "mine" in path.read_text(encoding="utf-8") + text = path.read_text(encoding="utf-8") + assert "mine" in text + assert "c4" in text # migration re-adds bundled ids assert path == user_schemes_path(tmp_path) +def test_migrate_removes_orphaned_builtin_and_adds_missing(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + path = user_schemes_path(tmp_path) + path.write_text( + json.dumps( + { + "bpmn": { + "title": "BPMN 2.0", + "author": "md2gost", + "prefix": "@startuml\n", + "postfix": "\n@enduml", + "includes": ["BPMN.puml"], + }, + "mine": {"title": "x", "author": "", "prefix": "", "postfix": ""}, + }, + ensure_ascii=False, + ) + + "\n", + encoding="utf-8", + ) + result = migrate_user_schemes(tmp_path) + assert result.written + assert "bpmn" in result.removed + assert "c4" in result.added + schemes = load_schemes_from_path(path) + assert "bpmn" not in schemes + assert "mine" in schemes + assert schemes["mine"].title == "x" + bundled = load_schemes_from_path(bundled_schemes_path()) + for sid in bundled: + assert sid in schemes + + +def test_migrate_preserves_user_override_of_builtin(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + path = user_schemes_path(tmp_path) + path.write_text( + json.dumps( + { + "c4": { + "title": "My C4", + "author": "", + "docs": "custom docs", + "prefix": "@startuml\n", + "postfix": "\n@enduml", + "includes": [], + } + }, + ensure_ascii=False, + ) + + "\n", + encoding="utf-8", + ) + result = migrate_user_schemes(tmp_path) + schemes = load_schemes_from_path(path) + assert schemes["c4"].docs == "custom docs" + assert schemes["c4"].author == "" + assert "c4" not in result.updated + assert "usecase" in schemes # other bundled ids still added + + +def test_migrate_updates_app_owned_builtin(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + path = user_schemes_path(tmp_path) + path.write_text( + json.dumps( + { + "c4": { + "title": "Old C4", + "author": "md2gost", + "version": "0.0.1", + "docs": "stale", + "prefix": "@startuml\n", + "postfix": "\n@enduml", + "includes": [], + } + }, + ensure_ascii=False, + ) + + "\n", + encoding="utf-8", + ) + result = migrate_user_schemes(tmp_path) + assert "c4" in result.updated + schemes = load_schemes_from_path(path) + bundled = load_schemes_from_path(bundled_schemes_path()) + assert schemes["c4"].to_dict() == bundled["c4"].to_dict() + + +def test_migrate_idempotent(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + ensure_user_schemes(tmp_path) + path = user_schemes_path(tmp_path) + before = path.read_text(encoding="utf-8") + mtime1 = path.stat().st_mtime_ns + result = migrate_user_schemes(tmp_path) + assert not result.written + assert not result.changed + assert path.read_text(encoding="utf-8") == before + assert path.stat().st_mtime_ns == mtime1 + + +def test_claim_user_scheme_if_diverged(): + bundled = load_schemes_from_path(bundled_schemes_path()) + stock = bundled["c4"] + same = DiagramScheme.from_dict("c4", stock.to_dict()) + claimed_same = claim_user_scheme_if_diverged(same, bundled=bundled) + assert claimed_same.author == "md2gost" + edited = DiagramScheme.from_dict( + "c4", + {**stock.to_dict(), "docs": "my docs"}, + ) + claimed = claim_user_scheme_if_diverged(edited, bundled=bundled) + assert claimed.author == "" + assert claimed.docs == "my docs" + + def test_apply_scheme_no_double_start(): scheme = DiagramScheme( id="t", @@ -137,6 +247,18 @@ def test_apply_scheme_no_double_start(): assert "skinparam monochrome true" in out +def test_archimate_scheme_includes_stdlib(): + bundled = load_schemes_from_path(bundled_schemes_path()) + scheme = bundled["archimate"] + assert scheme.title == "ArchiMate 3.2" + out = apply_scheme(scheme, 'Business_Actor(a, "X")\n') + assert "!include " in out + assert "Business_Actor(a, \"X\")" in out + assert out.strip().lower().startswith("@startuml") + assert "@enduml" in out.lower() + assert out.lower().count("@startuml") == 1 + + def test_include_cache_index_no_overwrite(tmp_path, monkeypatch): monkeypatch.chdir(tmp_path) calls = [] @@ -455,39 +577,15 @@ def test_diagram_engine_status_mentions_java_or_kroki(): def test_diagram_langs(): assert "uml" in DIAGRAM_LANGS assert "c4" in DIAGRAM_LANGS - assert "bpmn" in DIAGRAM_LANGS + assert "usecase" in DIAGRAM_LANGS + assert "idef0" in DIAGRAM_LANGS + assert "dfd" in DIAGRAM_LANGS assert is_diagram_lang("uml") assert is_diagram_lang("uml-c4") - assert is_diagram_lang("uml-bpmn") - - -def test_render_bpmn_with_jar(tmp_path, monkeypatch): - import shutil - - monkeypatch.chdir(tmp_path) - ensure_user_schemes(tmp_path) - configure_schemes(base=tmp_path) - jar = resolve_plantuml_jar(download=False) - if not shutil.which("java") or not jar: - pytest.skip("нужны Java и plantuml.jar") - configure_diagrams(fallback="off", cache_dir=str(tmp_path), plantuml_jar=jar) - body = "\n".join( - [ - "Start(s)", - "StartMessage(sm, \"msg\")", - "UserTask(t, \"Шаг\")", - "XOR(gw)", - "AND(p)", - "OR(o)", - "End(e)", - "EndTerminate(et)", - "Flow(s, t)", - "Flow(t, gw)", - "CondFlow(gw, e, \"да\")", - "DefaultFlow(gw, et)", - ] - ) - result = render_diagram("bpmn", body, cache_dir=str(tmp_path), fallback="off") - data = Path(result.png_path).read_bytes() - assert data.startswith(b"\x89PNG") - assert len(data) > 500 + assert is_diagram_lang("uml-usecase") + assert is_diagram_lang("idef0") + assert is_diagram_lang("uml-idef0") + assert is_diagram_lang("dfd") + assert is_diagram_lang("uml-dfd") + assert is_diagram_lang("data-flow-diagram") + assert is_diagram_lang("yourdon") diff --git a/tests/test_doc_metadata.py b/tests/test_doc_metadata.py new file mode 100644 index 0000000..2acfdfc --- /dev/null +++ b/tests/test_doc_metadata.py @@ -0,0 +1,228 @@ +"""DOCX core properties: resolve, apply, persist in user profile.""" + +from __future__ import annotations + +from datetime import datetime +from pathlib import Path + +from docx import Document + +from md2gost.doc_metadata import ( + CORE_PROP_MAX, + DEFAULT_AUTHOR_SOURCE, + DEFAULT_DOC_COMMENTS, + DocumentMetadata, + apply_document_metadata, + clip_core, + resolve_document_metadata, +) +from md2gost.pipeline import ConvertRequest, convert +from md2gost.user_profile import UserProfile, load_user_profile, save_user_profile +from md2gost.__main__ import build_parser, request_from_args + + +def test_clip_core_limit(): + assert clip_core("ab") == "ab" + assert len(clip_core("x" * (CORE_PROP_MAX + 10))) == CORE_PROP_MAX + + +def test_metadata_from_dict_defaults_and_empty_comments(): + blank = DocumentMetadata.from_dict(None) + assert blank.author_source == DEFAULT_AUTHOR_SOURCE + assert blank.comments == DEFAULT_DOC_COMMENTS + empty = DocumentMetadata.from_dict({"comments": "", "author_source": "nope"}) + assert empty.comments == "" + assert empty.author_source == DEFAULT_AUTHOR_SOURCE + custom = DocumentMetadata.from_dict({"author_source": "student", "title": " Отчёт "}) + assert custom.author_source == "student" + assert custom.title == "Отчёт" + + +def test_resolve_author_sources(monkeypatch): + monkeypatch.setattr("md2gost.doc_metadata.os_user", lambda: "win-user") + stored = DocumentMetadata(author_source="custom", author="Кастом К. К.") + os_meta = resolve_document_metadata(stored=DocumentMetadata(author_source="os")) + assert os_meta.author == "win-user" + assert os_meta.last_modified_by == "win-user" + assert os_meta.comments == DEFAULT_DOC_COMMENTS + + student_meta = resolve_document_metadata( + stored=DocumentMetadata(author_source="student"), + student="Студентов С. С.", + ) + assert student_meta.author == "Студентов С. С." + + custom_meta = resolve_document_metadata(stored=stored) + assert custom_meta.author == "Кастом К. К." + + override = resolve_document_metadata( + stored=stored, + author="С CLI", + comments="", + title="Тема работы", + last_modified_by="Редактор", + ) + assert override.author == "С CLI" + assert override.comments == "" + assert override.title == "Тема работы" + assert override.last_modified_by == "Редактор" + + empty_custom = resolve_document_metadata( + stored=DocumentMetadata(author_source="custom", author=""), + ) + assert empty_custom.author == "win-user" + + +def test_apply_document_metadata_writes_core_props(tmp_path: Path): + from md2gost import package_dir + + doc = Document(str(Path(package_dir()) / "Template.docx")) + stamp = datetime(2026, 9, 8, 12, 0, 0) + apply_document_metadata( + doc, + DocumentMetadata( + author="Автор А. А.", + last_modified_by="Автор А. А.", + title="Название", + subject="Тема", + keywords="гост, отчёт", + comments="", + category="учебный", + ), + now=stamp, + ) + out = tmp_path / "meta.docx" + doc.save(str(out)) + loaded = Document(str(out)) + cp = loaded.core_properties + assert cp.author == "Автор А. А." + assert cp.last_modified_by == "Автор А. А." + assert cp.title == "Название" + assert cp.subject == "Тема" + assert cp.keywords == "гост, отчёт" + assert cp.comments == "" + assert cp.category == "учебный" + assert cp.revision == 1 + assert cp.created.replace(tzinfo=None) == stamp + assert cp.modified.replace(tzinfo=None) == stamp + + +def test_user_profile_preserves_metadata_when_saving_student(tmp_path: Path): + save_user_profile( + UserProfile( + student="А А А", + group="ГР-1", + metadata=DocumentMetadata( + author_source="student", + title="Отчёт", + comments="", + ), + ), + base=tmp_path, + ) + loaded = load_user_profile(base=tmp_path) + assert loaded.student == "А А А" + assert loaded.metadata.author_source == "student" + assert loaded.metadata.title == "Отчёт" + assert loaded.metadata.comments == "" + + loaded.student = "Б Б Б" + save_user_profile(loaded, base=tmp_path) + again = load_user_profile(base=tmp_path) + assert again.student == "Б Б Б" + assert again.group == "ГР-1" + assert again.metadata.title == "Отчёт" + assert again.metadata.comments == "" + + +def test_old_user_json_without_metadata_block(tmp_path: Path): + path = tmp_path / "md2gost.user.json" + path.write_text('{"student": "Иванов", "group": "ИНБО-01-00"}\n', encoding="utf-8") + loaded = load_user_profile(base=tmp_path) + assert loaded.student == "Иванов" + assert loaded.metadata.comments == DEFAULT_DOC_COMMENTS + assert loaded.metadata.author_source == DEFAULT_AUTHOR_SOURCE + + +def test_parser_doc_metadata_flags(): + parser = build_parser() + args = parser.parse_args( + [ + "report.md", + "--doc-author-from", "student", + "--doc-author", "Петров П. П.", + "--doc-title", "Курсовая", + "--doc-comments", "", + "--doc-keywords", "гост", + ] + ) + req = request_from_args(args) + assert req.doc_author_source == "student" + assert req.doc_author == "Петров П. П." + assert req.doc_title == "Курсовая" + assert req.doc_comments == "" + assert req.doc_keywords == "гост" + default = request_from_args(parser.parse_args(["report.md"])) + assert default.doc_author is None + assert default.doc_comments is None + + +def test_pipeline_applies_profile_and_cli_metadata(tmp_path: Path, monkeypatch): + from md2gost import user_profile as up + + monkeypatch.setattr(up, "app_dir", lambda: tmp_path) + save_user_profile( + UserProfile( + student="Конвейев К. К.", + group="ИНБО-00-00", + metadata=DocumentMetadata( + author_source="student", + title="Из профиля", + subject="Тема профиля", + comments="штамп", + ), + ), + base=tmp_path, + ) + md = tmp_path / "report.md" + md.write_text("# *ВВЕДЕНИЕ\n\nТекст.\n", encoding="utf-8") + out = tmp_path / "report.docx" + result = convert( + ConvertRequest( + filename=str(md), + output=str(out), + table_continuation="off", + listing_continuation="off", + open_when_done=False, + ), + log=lambda _m: None, + ) + assert result.ok, result.message + cp = Document(str(out)).core_properties + assert cp.author == "Конвейев К. К." + assert cp.last_modified_by == "Конвейев К. К." + assert cp.title == "Из профиля" + assert cp.subject == "Тема профиля" + assert cp.comments == "штамп" + assert cp.revision == 1 + + out2 = tmp_path / "report2.docx" + result2 = convert( + ConvertRequest( + filename=str(md), + output=str(out2), + table_continuation="off", + listing_continuation="off", + open_when_done=False, + doc_author="С CLI", + doc_comments="", + doc_title="С CLI название", + ), + log=lambda _m: None, + ) + assert result2.ok, result2.message + cp2 = Document(str(out2)).core_properties + assert cp2.author == "С CLI" + assert cp2.comments == "" + assert cp2.title == "С CLI название" + assert cp2.subject == "Тема профиля" diff --git a/tests/test_idef0.py b/tests/test_idef0.py new file mode 100644 index 0000000..88f0bf0 --- /dev/null +++ b/tests/test_idef0.py @@ -0,0 +1,422 @@ +"""Parse and render IDEF0 fenced diagrams.""" + +from __future__ import annotations + +from pathlib import Path +from unittest.mock import patch + +import pytest +from PIL import Image + +from md2gost.diagram_renderer import ( + DIAGRAM_LANGS, + configure_diagrams, + is_diagram_lang, + prepare_source, + render_diagram, +) +from md2gost.extended_markdown import markdown +from md2gost.extended_markdown.caption import Caption as CapEl +from md2gost.idef0 import ( + SAMPLE_CONTEXT, + SAMPLE_DECOMPOSITION, + Idef0ParseError, + parse_idef0, + render_idef0, + write_idef0_outputs, + _Rect, + _layout_arrow_label, + _load_font, + _polyline_backtracks, + _seg_hits_open_box, + _layout_boxes, + _assign_slots, + _box_order, + _draw_arrow, + _make_channels, + _draw_kit_form, + _Scene, + _port_point, +) +from md2gost.renderable.caption import CaptionInfo +from md2gost.renderable.diagram import DiagramFigure +from md2gost.renderable_factory import RenderableFactory + + +def _svg_has_words(svg: str, phrase: str) -> None: + missing = [tok for tok in phrase.split() if tok and tok not in svg] + assert not missing, f"missing {missing} in SVG for {phrase!r}" + + +def test_is_diagram_lang_idef0(): + assert is_diagram_lang("idef0") + assert is_diagram_lang("uml-idef0") + assert "idef0" in DIAGRAM_LANGS + + +def test_prepare_idef0_stays_native(): + src, dtype = prepare_source("idef0", SAMPLE_CONTEXT) + assert dtype == "idef0" + assert "@startuml" not in src.lower() + assert "Распорядиться товаром" in src + src2, dtype2 = prepare_source("uml-idef0", "[A0] X\n<- in") + assert dtype2 == "idef0" + + +def test_parse_sample_context(): + d = parse_idef0(SAMPLE_CONTEXT) + assert d.node == "A-0" + assert d.title == "Распорядиться товаром" + assert d.number == "1" + assert d.author == "Кинзябулатов Рамиль" + assert "торгового предприятия" in d.project + assert d.date == "08.02.2022" + assert d.rev == "31.03.2022" + assert d.status == "PUBLICATION" + assert d.context == "TOP" + assert len(d.boxes) == 1 + assert d.boxes[0].id == "A0" + assert d.boxes[0].name == "Распорядиться товаром" + labels = {a.label: a for a in d.arrows} + assert set(labels) == {"Спрос", "Нормативная документация", "Товар", "Персонал"} + assert labels["Спрос"].targets[0].side == "I" + assert labels["Нормативная документация"].targets[0].side == "C" + assert labels["Товар"].sources[0].side == "O" + assert labels["Персонал"].targets[0].side == "M" + + +def test_parse_sample_decomposition(): + d = parse_idef0(SAMPLE_DECOMPOSITION) + assert d.node == "A0" + assert [b.id for b in d.boxes] == ["A1", "A2", "A3"] + by_label = {a.label: a for a in d.arrows} + assert by_label["Нормативная документация"].icom == "C1" + assert {t.box for t in by_label["Нормативная документация"].targets} == { + "A1", "A2", "A3", + } + assert by_label["график поставок"].sources[0].box == "A1" + assert by_label["график поставок"].targets[0].box == "A2" + assert by_label["график поставок"].targets[0].side == "C" + assert by_label["план закупок"].targets[0].box == "A2" + assert by_label["товар к отгрузке"].sources[0].box == "A2" + assert by_label["Товар"].icom == "O1" + assert {t.box for t in by_label["Персонал"].targets} == {"A1", "A2", "A3"} + assert by_label["Склад"].targets[0].box == "A2" + assert by_label["Спрос"].icom == "I1" + + +def test_layout_boxes_cluster_is_compact_and_centered(): + d = parse_idef0(SAMPLE_DECOMPOSITION) + work = _Rect(0, 0, 1400, 720) + geoms = _layout_boxes(d, work, 1.0) + boxes = [geoms[b.id].rect for b in d.boxes] + x0, x1 = min(b.x for b in boxes), max(b.r for b in boxes) + y0, y1 = min(b.y for b in boxes), max(b.b for b in boxes) + assert x1 - x0 < work.w * 0.55 + assert y1 - y0 < work.h * 0.70 + assert abs((x0 + x1) / 2 - work.cx) < 80 + assert x0 > 80 + assert work.r - x1 > 70 + assert y0 > 60 + assert work.b - y1 > 70 + + +def test_output_icom_with_control_branch_also_hits_boundary(): + d = parse_idef0( + """ + [A1] One + [A2] Two + A1 -> A2.C : план продаж [O1] + """ + ) + arrow = d.arrows[0] + assert arrow.icom == "O1" + assert { (t.box, t.side) for t in arrow.targets } == {("A2", "C"), (None, "O")} + + +def test_arrow_label_sits_off_vertical_shaft(): + work = _Rect(0, 0, 800, 600) + font = _load_font(12, False) + placed = _layout_arrow_label( + [(200.0, 40.0), (200.0, 320.0)], + "Нормативная документация", + font, + 12.0, + 1.0, + work, + [], + ) + assert placed is not None + halo, attach, _connect, lines, orient = placed + assert orient == "V" + assert not halo.contains_point(*attach, pad=3) + assert abs(halo.cx - attach[0]) >= 10 + assert len(lines) >= 2 + + +def test_arrow_label_sits_above_horizontal_shaft(): + work = _Rect(0, 0, 800, 600) + font = _load_font(12, False) + placed = _layout_arrow_label( + [(40.0, 200.0), (360.0, 200.0)], + "Спрос", + font, + 12.0, + 1.0, + work, + [], + ) + assert placed is not None + halo, attach, _connect, _lines, orient = placed + assert orient == "H" + assert halo.b <= attach[1] - 4 or halo.y >= attach[1] + 4 + assert not halo.contains_point(*attach, pad=2) + + +def test_decomposition_paths_do_not_backtrack_or_cut_boxes(): + d = parse_idef0(SAMPLE_DECOMPOSITION) + s = 1.0 + w, h = 1400, 920 + scene = _Scene(w, h) + fonts = { + "lab": 9, "val": 11, "small": 9.5, "box": 13, "id": 10, + "arrow": 12, "node": 20, "title": 16, "top": 22, + } + work = _draw_kit_form(scene, d, _Rect(8, 8, w - 16, h - 16), s, fonts) + geoms = _layout_boxes(d, work, s) + _assign_slots(d, geoms) + order = _box_order(d) + ch = _make_channels(d, geoms, work, s) + for arrow in d.arrows: + paths = _draw_arrow(scene, arrow, geoms, work, s, 1.2, order, ch) + skip = {e.box for e in arrow.sources + arrow.targets if e.box} + for pts in paths: + assert not _polyline_backtracks(pts), arrow.label + for a, b in zip(pts, pts[1:]): + for gid, g in geoms.items(): + if gid in skip: + continue + assert not _seg_hits_open_box(a, b, g.rect), (arrow.label, gid) + + +def test_two_ports_on_same_side_are_spread(): + d = parse_idef0( + """ + [A0] Box + <- one + <- two + -> out + ^ ctrl + v mech + """ + ) + work = _Rect(0, 0, 800, 600) + geoms = _layout_boxes(d, work, 1.0) + _assign_slots(d, geoms) + g = geoms["A0"] + y0, y1 = _port_point(g, "I", 0, 2)[1], _port_point(g, "I", 1, 2)[1] + assert abs(y1 - y0) >= 0.5 * g.rect.h + + +def test_parse_keyword_and_explicit_box(): + d = parse_idef0( + """ + [A0] Распорядиться товаром + A0 in Спрос + A0 control Нормативная документация + A0 out Товар + A0 mech Персонал + """ + ) + assert [a.label for a in d.arrows] == [ + "Спрос", + "Нормативная документация", + "Товар", + "Персонал", + ] + + +def test_parse_decomposition_fork_join_icom_tunnel(): + d = parse_idef0( + """ + node A0 + title Обработать заказ + number 2 + + [A1] Принять заказ + [A2] Закупить + [A3] Отгрузить + + A1 <- Заявка [I1] + A1 ^ Правила [C1] + A1 -> A2 : потребность + A2 -> A3 : товар на складе + A3 -> Товар [O1] + A1, A2 -> A3 : сводка + A1 -> A2, A3 : данные + (in Секрет + in) Локальный + A3 v Склад [M1] + """ + ) + assert d.node == "A0" + assert [b.id for b in d.boxes] == ["A1", "A2", "A3"] + by_label = {a.label: a for a in d.arrows} + assert by_label["потребность"].sources[0].box == "A1" + assert by_label["потребность"].targets[0].box == "A2" + assert by_label["Заявка"].icom == "I1" + assert {t.box for t in by_label["данные"].targets} == {"A2", "A3"} + assert {s.box for s in by_label["сводка"].sources} == {"A1", "A2"} + secret = by_label["Секрет"] + assert secret.sources[0].box is None and secret.sources[0].tunnel + local = by_label["Локальный"] + assert local.targets[0].tunnel + + +def test_parse_empty_raises(): + with pytest.raises(Idef0ParseError, match="пустой"): + parse_idef0(" \n # only comment\n") + + +def test_parse_bad_line_raises(): + with pytest.raises(Idef0ParseError, match="строку"): + parse_idef0("[A0] Box\nthis is not an arrow\n") + + +def test_render_sample_png_and_svg(tmp_path): + d = parse_idef0(SAMPLE_CONTEXT) + png, svg = render_idef0(d, scale=1.0, want_svg=True) + assert png.startswith(b"\x89PNG") + assert svg is not None + for needle in ( + "Распорядиться товаром", + "A-0", + "Спрос", + "Нормативная документация", + "Товар", + "Персонал", + "PUBLICATION", + "TOP", + "A0", + "Кинзябулатов", + ): + _svg_has_words(svg, needle) + img = Image.open(__import__("io").BytesIO(png)) + assert img.size[0] >= 1000 + assert img.size[1] >= 700 + extrema = img.convert("L").getextrema() + assert extrema[0] < 250 # has ink, not a blank page + + out_png = tmp_path / "a.png" + out_svg = tmp_path / "a.svg" + assert write_idef0_outputs(SAMPLE_CONTEXT, out_png, out_svg, scale=1) + assert out_png.read_bytes().startswith(b"\x89PNG") + assert "NODE" in out_svg.read_text(encoding="utf-8") + + +def test_render_decomposition_does_not_crash(): + src = """ + node A0 + title Распорядиться товаром + [A1] Принять заказ + [A2] Отгрузить + [A3] Учесть + A1 <- Заявка [I1] + A1 -> A2 : заказ + A2 -> A3 : накладная + A3 -> Товар [O1] + A1 ^ Правила [C1] + A2 v Склад [M1] + """ + png, svg = render_idef0(parse_idef0(src), scale=1.0, want_svg=True) + assert png.startswith(b"\x89PNG") + assert "Принять заказ" in (svg or "") + assert "заказ" in (svg or "") + + +def test_render_sample_decomposition_png_and_svg(): + png, svg = render_idef0(parse_idef0(SAMPLE_DECOMPOSITION), scale=1.0, want_svg=True) + assert png.startswith(b"\x89PNG") + text = svg or "" + for needle in ( + "Принять и оценить спрос", + "Закупить и хранить", + "план закупок", + "график поставок", + "Персонал", + "C1", + "A3", + ): + _svg_has_words(text, needle) + assert "Purpose:" not in text + assert "Viewpoint:" not in text + assert "A4" not in text + img = Image.open(__import__("io").BytesIO(png)) + assert img.size[0] >= 1400 + assert img.size[1] >= 900 + extrema = img.convert("L").getextrema() + assert extrema[0] < 250 + + +def test_render_diagram_idef0_skips_jar_and_kroki(tmp_path): + calls = {"jar": 0, "kroki": 0} + + def fake_jar(*args, **kwargs): + calls["jar"] += 1 + return False + + def fake_kroki(*args, **kwargs): + calls["kroki"] += 1 + return False + + configure_diagrams(fallback="off", cache_dir=str(tmp_path), diagram_scale=1) + with patch("md2gost.diagram_renderer._render_plantuml_jar", side_effect=fake_jar), \ + patch("md2gost.diagram_renderer._render_kroki", side_effect=fake_kroki): + result = render_diagram( + "idef0", SAMPLE_CONTEXT, cache_dir=str(tmp_path), diagram_scale=1, + ) + assert calls["jar"] == 0 + assert calls["kroki"] == 0 + assert Path(result.png_path).read_bytes().startswith(b"\x89PNG") + assert result.pixel_scale == 1 + + +def test_render_diagram_idef0_scale_and_cache(tmp_path): + configure_diagrams(fallback="off", cache_dir=str(tmp_path), diagram_scale=2) + r1 = render_diagram("idef0", SAMPLE_CONTEXT, cache_dir=str(tmp_path), diagram_scale=2) + r2 = render_diagram("idef0", SAMPLE_CONTEXT, cache_dir=str(tmp_path), diagram_scale=2) + assert r1.png_path == r2.png_path + assert r1.pixel_scale == 2 + img = Image.open(r1.png_path) + assert img.size[0] >= 2000 + + +def test_render_diagram_idef0_bad_source(tmp_path): + configure_diagrams(fallback="off", cache_dir=str(tmp_path)) + with pytest.raises(RuntimeError, match="IDEF0"): + render_diagram("idef0", "not a diagram", cache_dir=str(tmp_path)) + + +def test_factory_routes_idef0_to_diagram(): + from docx import Document + from md2gost import package_dir + import os + + md = "%ctx Контекст A-0\n\n```idef0\n" + SAMPLE_CONTEXT + "```\n" + parsed = markdown.parse(md) + doc = Document(os.path.join(package_dir(), "Template.docx")) + factory = RenderableFactory(doc._body) + caption = None + renderables = [] + for el in parsed.children: + from marko.block import BlankLine + if isinstance(el, BlankLine): + continue + if isinstance(el, CapEl): + caption = CaptionInfo(el.unique_name, el.text, el.with_listing) + continue + renderables.append(factory.create(el, caption)) + caption = None + assert len(renderables) == 1 + assert isinstance(renderables[0], DiagramFigure) + assert renderables[0]._lang == "idef0" diff --git a/tests/test_landscape.py b/tests/test_landscape.py index 9f4137e..1c8fc56 100644 --- a/tests/test_landscape.py +++ b/tests/test_landscape.py @@ -16,6 +16,14 @@ from md2gost.renderable.caption import CaptionInfo from md2gost.renderable.image import Image from md2gost.renderable.table import Table from md2gost.renderer import Renderer +from docx.oxml.ns import qn + + +def _section_page_start(section): + pg = section._sectPr.find(qn("w:pgNumType")) + if pg is None: + return None + return pg.get(qn("w:start")) def test_content_size_landscape_wider(): @@ -57,6 +65,50 @@ def test_renderer_landscape_section_for_table(): assert doc.sections[-1].orientation == WD_ORIENT.PORTRAIT assert int(doc.sections[-1].page_width) < int(doc.sections[-1].page_height) + # Landscape and return-to-portrait must not restart PAGE (no w:start). + for section in land2: + assert _section_page_start(section) is None + assert _section_page_start(doc.sections[-1]) is None + + +def test_landscape_sections_have_no_page_restart(): + """+landscape must not leave w:pgNumType/@w:start on album / following portrait.""" + doc = Document(os.path.join(package_dir(), "Template.docx")) + body = doc._body._element + for child in list(body): + if child.tag.endswith("}sectPr"): + continue + body.remove(child) + + from md2gost.renderable.heading import Heading + from md2gost.renderable.paragraph import Paragraph + from md2gost.renderable.toc import ToC + + h = Heading(doc._body, 1, False) + h.add_run("СОДЕРЖАНИЕ") + toc = ToC(doc._body, toc_mode="native") + para = Paragraph(doc._body) + para.add_run("Текст перед широкой таблицей.") + table = Table(doc._body, 2, 3, CaptionInfo("wide", "Карта", landscape=True)) + for r in range(2): + for c in range(3): + table.add_paragraph_to_cell(r, c).add_run(f"{r}{c}") + after = Paragraph(doc._body) + after.add_run("Текст после альбома.") + + Renderer(doc, skip_numbering=True).process([h, toc, para, table, after]) + + land = [s for s in doc.sections if s.orientation == WD_ORIENT.LANDSCAPE] + assert land + for section in land: + assert _section_page_start(section) is None, "landscape restarted PAGE" + # Body portrait after landscape also continuous. + assert doc.sections[-1].orientation == WD_ORIENT.PORTRAIT + assert _section_page_start(doc.sections[-1]) is None + # Non-first sections must not restart (first may still carry template start). + for section in list(doc.sections)[1:]: + assert _section_page_start(section) is None + def test_parser_propagates_landscape_to_table(): doc = Document(os.path.join(package_dir(), "Template.docx")) diff --git a/tests/test_mirea_tz.py b/tests/test_mirea_tz.py index 9666395..fd244b8 100644 --- a/tests/test_mirea_tz.py +++ b/tests/test_mirea_tz.py @@ -201,6 +201,15 @@ def test_preprocess_skips_uml_and_mermaid_fences(): "```\n\n" "```uml-c4context\n" 'Person(user, "Заявитель")\n' + "```\n\n" + "```idef0\n" + 'title "Выручка"\n' + "[A0] Процесс\n" + "```\n\n" + "```dfd\n" + 'entity User\n' + 'process App\n' + 'User --> App "request"\n' "```\n" ) out = preprocess_markdown(md) @@ -209,6 +218,10 @@ def test_preprocess_skips_uml_and_mermaid_fences(): assert 'title "Выручка"' in out assert 'Person(user, "Заявитель")' in out assert "«Акционеры»" not in out + assert "[A0] Процесс" in out + assert "entity User" in out + assert 'User --> App "request"' in out + assert "«request»" not in out def test_formula_refs(): @@ -247,6 +260,42 @@ def test_biblio_entries_from_concatenated_paragraph(): assert flat[0].key == "1" +def test_fold_bibliography_drops_citation_mangled_residue(): + """Citation linking turns [5]: into []: in .text — must still be consumed.""" + import docx + from md2gost.biblio_processor import ( + fold_bibliography, + is_biblio_source_paragraph, + ) + from md2gost.bibliography_renderable import Bibliography + from md2gost.renderable.heading import Heading + from md2gost.renderable.paragraph import Paragraph + + assert is_biblio_source_paragraph("[1]: Книга. — М., 2024.") + assert is_biblio_source_paragraph("[]: Книга. — М., 2024.") + assert is_biblio_source_paragraph("[]: https://example.com/a") + assert not is_biblio_source_paragraph("Обычный абзац без источника.") + + doc = docx.Document(r"md2gost/Template.docx") + doc._body.clear_content() + h = Heading(doc._body, 1, numbered=False) + h.add_run("СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ") + residue = Paragraph(doc._body) + residue.add_run("[]: Остаток после гиперссылки на цитату.") + prose = Paragraph(doc._body) + prose.add_run("Это уже не библиография.") + + md = """# *СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ + +[1]: Книга. — М., 2024. +""" + out = fold_bibliography([h, residue, prose], doc._body, raw_markdown=md) + types = [type(x).__name__ for x in out] + assert types == ["Heading", "Bibliography", "Paragraph"] + assert isinstance(out[1], Bibliography) + assert out[2] is prose + + def test_emdash_to_hyphen_preprocess(): from md2gost.profiles import preprocess_markdown, set_emdash_to_hyphen, dash_separator out = preprocess_markdown("Цель — тест. Слово - слово.", emdash_to_hyphen=True) diff --git a/tests/test_page_geometry.py b/tests/test_page_geometry.py index 89f2402..33364e0 100644 --- a/tests/test_page_geometry.py +++ b/tests/test_page_geometry.py @@ -1,9 +1,28 @@ """Page number start / offset helpers.""" +from pathlib import Path +from zipfile import ZipFile + from docx import Document from docx.oxml.ns import qn +from lxml import etree -from md2gost.page_geometry import apply_page_number_start, set_section_page_start +from md2gost.page_geometry import ( + apply_page_number_start, + apply_section_geometry, + clear_section_page_start, + patch_docx_page_starts, + set_section_page_start, +) + +_W = "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}" + + +def _pg_start(section): + pg = section._sectPr.find(qn("w:pgNumType")) + if pg is None: + return None + return pg.get(qn("w:start")) def test_set_section_page_start(): @@ -33,3 +52,96 @@ def test_apply_page_number_start_none_strips(): apply_page_number_start(doc, None) pg = doc.sections[0]._sectPr.find(qn("w:pgNumType")) assert pg is None or pg.get(qn("w:start")) is None + + +def test_apply_page_number_start_none_strips_all_four_sections(): + doc = Document() + for _ in range(3): + doc.add_section() + for section in doc.sections: + set_section_page_start(section, 1) + assert all(_pg_start(s) == "1" for s in doc.sections) + + apply_page_number_start(doc, None) + assert all(_pg_start(s) is None for s in doc.sections) + + +def test_apply_page_number_start_body_only_with_front(): + doc = Document() + for _ in range(3): + doc.add_section() # front + body + landscape + portrait = 4 + for section in doc.sections: + set_section_page_start(section, 1) + + apply_page_number_start(doc, 1, front_sections=1) + assert _pg_start(doc.sections[0]) is None + assert _pg_start(doc.sections[1]) == "1" + assert _pg_start(doc.sections[2]) is None + assert _pg_start(doc.sections[3]) is None + + +def test_apply_section_geometry_strips_copied_start(): + doc = Document() + set_section_page_start(doc.sections[0], 1) + apply_section_geometry(doc.sections[0], landscape=True) + assert _pg_start(doc.sections[0]) is None + + +def test_clear_section_page_start_removes_empty_element(): + doc = Document() + set_section_page_start(doc.sections[0], 1) + clear_section_page_start(doc.sections[0]) + assert doc.sections[0]._sectPr.find(qn("w:pgNumType")) is None + + +def test_patch_docx_page_starts_strips_and_restores(tmp_path: Path): + doc = Document() + for _ in range(3): + doc.add_section() + for section in doc.sections: + set_section_page_start(section, 1) + path = tmp_path / "pages.docx" + doc.save(path) + + patch_docx_page_starts(path, None) + with ZipFile(path) as z: + root = etree.fromstring(z.read("word/document.xml")) + starts = [ + el.get(f"{_W}start") + for el in root.findall(f".//{_W}pgNumType") + ] + assert all(s is None for s in starts) + + # Re-seed starts then restore body-only restart. + doc2 = Document(str(path)) + for section in doc2.sections: + set_section_page_start(section, 1) + doc2.save(path) + + patch_docx_page_starts(path, 2, front_sections=1) + with ZipFile(path) as z: + root = etree.fromstring(z.read("word/document.xml")) + body = root.find(f"{_W}body") + sects = [] + for child in body: + if child.tag == f"{_W}sectPr": + sects.append(child) + continue + if child.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) + assert len(sects) == 4 + vals = [ + (s.find(f"{_W}pgNumType").get(f"{_W}start") + if s.find(f"{_W}pgNumType") is not None else None) + for s in sects + ] + assert vals[0] is None + assert vals[1] == "2" + assert vals[2] is None + assert vals[3] is None diff --git a/tests/test_pipeline.py b/tests/test_pipeline.py index b6c0e32..65511c7 100644 --- a/tests/test_pipeline.py +++ b/tests/test_pipeline.py @@ -156,6 +156,7 @@ def test_prompt_catalog_loads(): assert "generate-md.md" in names assert "generate-mirea-report.md" in names assert "generate-pis-custom-report.md" in names + assert "emulate-student.md" in names assert all(text.strip() for _n, _t, text in catalog) diff --git a/tests/test_title_page.py b/tests/test_title_page.py new file mode 100644 index 0000000..7354c66 --- /dev/null +++ b/tests/test_title_page.py @@ -0,0 +1,291 @@ +"""Tests for title page generation (```title, info_conv.yaml, template fill).""" + +from __future__ import annotations + +from pathlib import Path + +from docx import Document + +from md2gost.title_page import ( + TitleMeta, + build_title_docx, + load_info_conv, + merge_title_meta, + parse_and_strip_title_fence, + parse_kv_text, + should_generate_title, +) +from md2gost.user_profile import UserProfile, load_user_profile, save_user_profile + + +def test_parse_title_fence_and_strip(): + md = ( + "```title\n" + "number: 3\n" + "year: 2025\n" + "discipline: Тестовая дисциплина\n" + "```\n" + "\n" + "# *ВВЕДЕНИЕ\n" + "\n" + "Text.\n" + ) + fields, body = parse_and_strip_title_fence(md) + assert fields["number"] == "3" + assert fields["year"] == "2025" + assert fields["discipline"] == "Тестовая дисциплина" + assert "# *ВВЕДЕНИЕ" in body + assert "```title" not in body + + +def test_parse_kv_russian_aliases_and_typos(): + raw = ( + "Инститиут: ИИТ\n" + "Кафедра: ЦТ\n" + "Дисциплина: ПАЦО\n" + "Преподователь: Иванов И. И.\n" + "Должность: старший преподаватель\n" + ) + y = parse_kv_text(raw) + assert y["institute"] == "ИИТ" + assert y["department"] == "ЦТ" + assert y["discipline"] == "ПАЦО" + assert y["teacher"] == "Иванов И. И." + assert y["teacher_position"] == "старший преподаватель" + + +def test_merge_fence_overrides_yaml(): + yaml_f = {"institute": "ИИТ", "discipline": "Из yaml"} + fence = {"discipline": "Из fence", "number": "2"} + meta = merge_title_meta( + yaml_fields=yaml_f, + fence_fields=fence, + student="Петров П. П.", + group="ИНБО-01-20", + ) + assert meta.institute == "ИИТ" + assert meta.discipline == "Из fence" + assert meta.number == "2" + assert meta.student == "Петров П. П." + assert meta.group == "ИНБО-01-20" + # student/group ignored from yaml/fence + meta2 = merge_title_meta( + yaml_fields={"student": "X", "group": "Y", "institute": "Z"}, + student="A", + group="B", + ) + assert meta2.student == "A" and meta2.group == "B" + assert meta2.institute == "Z" + + +def test_load_info_conv(tmp_path: Path): + p = tmp_path / "info_conv.yaml" + p.write_text( + "Институт: Институт информационных технологий (ИИТ)\n" + "Кафедра: Кафедра ЦТ\n" + "Преподаватель: Сидоров\n" + "Должность: ассистент\n", + encoding="utf-8", + ) + data = load_info_conv(tmp_path) + assert "ИИТ" in data["institute"] + assert data["teacher"] == "Сидоров" + + +def test_should_generate_title(tmp_path: Path): + assert should_generate_title( + explicit_title="ready.docx", + fence_fields={"number": "1"}, + md_dir=tmp_path, + enabled=True, + ) is False + assert should_generate_title( + explicit_title=None, + fence_fields={"number": "1"}, + md_dir=tmp_path, + enabled=False, + ) is False + assert should_generate_title( + explicit_title=None, + fence_fields={"number": "1"}, + md_dir=tmp_path, + enabled=True, + ) is True + assert should_generate_title( + explicit_title=None, + fence_fields={}, + md_dir=tmp_path, + enabled=True, + ) is False + (tmp_path / "info_conv.yaml").write_text("Институт: X\n", encoding="utf-8") + assert should_generate_title( + explicit_title=None, + fence_fields={}, + md_dir=tmp_path, + enabled=True, + ) is True + assert should_generate_title( + explicit_title=None, + fence_fields={}, + md_dir=tmp_path, + enabled=False, + ) is False + + +def test_user_profile_roundtrip(tmp_path: Path): + save_user_profile(UserProfile(student="А А А", group="ГР-1"), base=tmp_path) + loaded = load_user_profile(base=tmp_path) + assert loaded.student == "А А А" + assert loaded.group == "ГР-1" + assert loaded.is_complete() + + +def test_build_title_docx_fills_red_slots(tmp_path: Path): + meta = TitleMeta( + institute="Институт ИИТ", + department="Кафедра ЦТ", + discipline="Дисциплина X", + number="7", + group="ИНБО-99-99", + student="Тестов Тест Тестович", + teacher_position="доцент, кафедра ЦТ", + teacher="Преподов П. П.", + year="2024", + ) + out = tmp_path / "title.docx" + build_title_docx(meta, out) + doc = Document(str(out)) + texts = [p.text for p in doc.paragraphs] + assert "Институт ИИТ" in texts[0] + assert "Кафедра ЦТ" in texts[1] + assert "Дисциплина X" in texts[5] + assert "7" in texts[8] + assert "ИНБО-99-99" in texts[13] + assert "Тестов Тест Тестович" in texts[15] + assert "доцент" in texts[19] + assert "Преподов" in texts[21] + assert "2024" in texts[28] + # prefixes kept + assert texts[13].startswith("студент группы") + assert "по дисциплине" in texts[5] + assert "Москва" in texts[28] + + +def test_pipeline_generates_title(tmp_path: Path, monkeypatch): + from md2gost.pipeline import ConvertRequest, convert + from md2gost import user_profile as up + + monkeypatch.setattr(up, "app_dir", lambda: tmp_path) + save_user_profile( + UserProfile(student="Конвейев К. К.", group="ИНБО-00-00"), + base=tmp_path, + ) + (tmp_path / "info_conv.yaml").write_text( + "Институт: ИИТ тест\n" + "Кафедра: Каф тест\n" + "Дисциплина: Дисц тест\n" + "Преподаватель: Преп П. П.\n" + "Должность: ст. преподаватель\n", + encoding="utf-8", + ) + md = tmp_path / "report.md" + md.write_text( + "```title\n" + "number: 2\n" + "year: 2026\n" + "```\n" + "\n" + "# *СОДЕРЖАНИЕ\n" + "\n" + "[TOC]\n" + "\n" + "# *ВВЕДЕНИЕ\n" + "\n" + "Текст введения.\n" + "\n" + "# *ЗАКЛЮЧЕНИЕ\n" + "\n" + "Вывод.\n" + "\n" + "# *СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ\n" + "\n" + "[1]: Книга. — М., 2024.\n", + encoding="utf-8", + ) + out = tmp_path / "report.docx" + result = convert( + ConvertRequest( + filename=str(md), + output=str(out), + table_continuation="off", + listing_continuation="off", + auto_title=True, + open_when_done=False, + ), + log=lambda _m: None, + ) + assert result.ok, result.message + assert out.is_file() + doc = Document(str(out)) + blob = "\n".join(p.text for p in doc.paragraphs) + assert "Конвейев" in blob + assert "ИНБО-00-00" in blob + assert "ИИТ тест" in blob + assert "Дисц тест" in blob + assert "Преп П. П." in blob + + +def test_pipeline_skips_title_by_default(tmp_path: Path, monkeypatch): + from md2gost.pipeline import ConvertRequest, convert + from md2gost import user_profile as up + + monkeypatch.setattr(up, "app_dir", lambda: tmp_path) + save_user_profile( + UserProfile(student="Конвейев К. К.", group="ИНБО-00-00"), + base=tmp_path, + ) + (tmp_path / "info_conv.yaml").write_text( + "Институт: ИИТ тест\n" + "Кафедра: Каф тест\n" + "Дисциплина: Дисц тест\n" + "Преподаватель: Преп П. П.\n" + "Должность: ст. преподаватель\n", + encoding="utf-8", + ) + md = tmp_path / "report.md" + md.write_text( + "```title\n" + "number: 2\n" + "year: 2026\n" + "```\n" + "\n" + "# *ВВЕДЕНИЕ\n" + "\n" + "Текст введения.\n" + "\n" + "# *ЗАКЛЮЧЕНИЕ\n" + "\n" + "Вывод.\n" + "\n" + "# *СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ\n" + "\n" + "[1]: Книга. — М., 2024.\n", + encoding="utf-8", + ) + out = tmp_path / "report.docx" + result = convert( + ConvertRequest( + filename=str(md), + output=str(out), + table_continuation="off", + listing_continuation="off", + open_when_done=False, + ), + log=lambda _m: None, + ) + assert result.ok, result.message + doc = Document(str(out)) + blob = "\n".join(p.text for p in doc.paragraphs) + assert "Конвейев" not in blob + assert "ИИТ тест" not in blob + assert "Текст введения" in blob diff --git a/tests/test_word2md.py b/tests/test_word2md.py new file mode 100644 index 0000000..317b15a --- /dev/null +++ b/tests/test_word2md.py @@ -0,0 +1,378 @@ +"""Tests for word2md (DOCX → md2gost Markdown).""" + +from __future__ import annotations + +from pathlib import Path + +from docx import Document + +from word2md.captions import caption_unique_id, parse_any_caption +from word2md.classify import classify_heading_text, is_body_start_heading, is_special_title +from word2md.emit import emit_markdown +from word2md.pipeline import ImportRequest, convert_docx +from word2md.refs import apply_reference_pass, rewrite_text +from word2md.blocks import CaptionBlock, HeadingBlock, ParagraphBlock, InlineSpan, TableBlock, ImageBlock +from word2md.walker import postprocess_blocks +from word2md.tables import table_to_grid + + +def test_parse_captions(): + fig = parse_any_caption("Рисунок 1.1 — Архитектура") + assert fig is not None + assert fig.kind == "figure" + assert fig.number == "1.1" + assert fig.text == "Архитектура" + + tbl = parse_any_caption("Таблица 2 — Сравнение") + assert tbl is not None and tbl.kind == "table" and tbl.number == "2" + + cont = parse_any_caption("Продолжение Таблицы 2.1") + assert cont is not None and cont.is_continuation and cont.kind == "table" + + lst = parse_any_caption("Листинг 1 — Код") + assert lst is not None and lst.kind == "listing" + assert caption_unique_id("figure", "1.1") == "fig1_1" + + +def test_special_headings(): + assert is_special_title("ВВЕДЕНИЕ") + assert is_special_title("Список использованных источников") + level, title, numbered = classify_heading_text("ВВЕДЕНИЕ", 1) + assert level == 1 and not numbered and title == "ВВЕДЕНИЕ" + assert is_body_start_heading("ВВЕДЕНИЕ", "Heading 1") + + +def test_rewrite_refs(): + maps = { + "figure": {"1.1": "fig1_1", "3.1": "fig3_1"}, + "table": {"2": "tbl2", "1.1": "tbl1_1"}, + "listing": {}, + "equation": {"1": "eq1"}, + } + text = "См. Рисунок 1.1 и табл. 2, а также [1]." + out = rewrite_text(text, maps) + assert "@Рисунок:fig1_1" in out + assert "@Таблица:tbl2" in out + assert "[1]" in out + # trailing sentence period must not stick to the number + assert rewrite_text("сведены в Таблице 1.1.", maps) == "сведены в @Таблица:tbl1_1." + assert rewrite_text("показан на Рисунке 3.1.", maps) == "показан на @Рисунок:fig3_1." + + +def test_emit_special_and_table(): + blocks = [ + HeadingBlock(1, "СОДЕРЖАНИЕ", numbered=False), + HeadingBlock(1, "ВВЕДЕНИЕ", numbered=False), + ParagraphBlock(spans=[InlineSpan("Текст со ссылкой на Рисунок 1.")]), + CaptionBlock(kind="table", number="1", text="Сравнение", unique_id="tbl1"), + TableBlock(rows=[["A", "B"], ["1", "2"]], caption_id="tbl1", caption_text="Сравнение"), + ] + blocks = apply_reference_pass(blocks) + # no figure map → leave as is for figure + md = emit_markdown(blocks) + assert "# *СОДЕРЖАНИЕ" in md + assert "# *ВВЕДЕНИЕ" in md + assert "%tbl1 Сравнение" in md + assert "| A | B |" in md + + +def test_escape_literal_asterisk(): + """DOCX stores bare '*'; MD footnotes need '\\*' so Marko won't eat them.""" + from word2md.emit import escape_md_inline + + assert escape_md_inline("* Для 2025") == "\\* Для 2025" + assert escape_md_inline("1,50*") == "1,50\\*" + assert escape_md_inline("@Рисунок:fig1_1") == "@Рисунок:fig1_1" + + md = emit_markdown( + [ + ParagraphBlock(spans=[InlineSpan("* Для 2025 года приведён документооборот.")]), + TableBlock(rows=[["Показатель", "2025"], ["ЭДО", "1,50*"]]), + ] + ) + assert "\\* Для 2025 года" in md + assert "1,50\\*" in md + # dialect heading star stays structural + assert "# *ВВЕДЕНИЕ" == emit_markdown( + [HeadingBlock(1, "ВВЕДЕНИЕ", numbered=False)] + ).strip() + + +def test_postprocess_continuation_tables(): + blocks = [ + CaptionBlock(kind="table", number="1", text="T", unique_id="tbl1"), + TableBlock(rows=[["A"], ["1"]], caption_id="tbl1"), + CaptionBlock(kind="table", number="1", is_continuation=True, unique_id="tbl1"), + TableBlock(rows=[["2"]]), + ] + out = postprocess_blocks(blocks) + tables = [b for b in out if isinstance(b, TableBlock)] + assert len(tables) == 1 + assert tables[0].rows == [["A"], ["1"], ["2"]] + + +def test_postprocess_skips_repeated_header(): + blocks = [ + CaptionBlock(kind="table", number="1", text="T", unique_id="tbl1"), + TableBlock(rows=[["H1", "H2"], ["1", "a"]], caption_id="tbl1"), + CaptionBlock(kind="table", number="1", is_continuation=True, unique_id="tbl1"), + TableBlock(rows=[["H1", "H2"], ["2", "b"]]), + ] + out = postprocess_blocks(blocks) + tables = [b for b in out if isinstance(b, TableBlock)] + assert len(tables) == 1 + assert tables[0].rows == [["H1", "H2"], ["1", "a"], ["2", "b"]] + + +def test_fold_figure_caption_under_image(): + """GOST: picture then «Рисунок N — …» → one image with %id in title.""" + blocks = [ + ParagraphBlock(spans=[InlineSpan("См. Рисунок 1.1.")]), + ImageBlock(rel_path="media/a.png"), + CaptionBlock(kind="figure", number="1.1", text="Схема", unique_id="fig1_1"), + ] + out = postprocess_blocks(blocks) + assert len([b for b in out if isinstance(b, CaptionBlock)]) == 0 + imgs = [b for b in out if isinstance(b, ImageBlock)] + assert len(imgs) == 1 + assert imgs[0].caption_id == "fig1_1" + assert imgs[0].caption_text == "Схема" + out = apply_reference_pass(out) + md = emit_markdown(out) + assert "@Рисунок:fig1_1" in md + img_line = [l for l in md.splitlines() if l.startswith("![")][0] + assert "%fig1_1" in img_line + + +def test_sequential_figures_keep_distinct_ids(): + """Caption under fig N must not leak onto figure N+1 via pending.""" + blocks = [ + ImageBlock(rel_path="a.png"), + CaptionBlock(kind="figure", number="5.3", text="Архитектура", unique_id="fig5_3"), + ImageBlock(rel_path="b.png"), + CaptionBlock(kind="figure", number="5.2", text="DFD", unique_id="fig5_2"), + ] + out = postprocess_blocks(blocks) + imgs = [b for b in out if isinstance(b, ImageBlock)] + assert [i.caption_id for i in imgs] == ["fig5_3", "fig5_2"] + + +def test_docx_continuation_tables_merge(tmp_path: Path): + """«Продолжение Таблицы N» + fragment must become one markdown table.""" + doc = Document() + _ensure_styles(doc) + h = doc.add_paragraph("ВВЕДЕНИЕ") + try: + h.style = "Heading 1" + except KeyError: + pass + cap = doc.add_paragraph("Таблица 1 — Демо") + try: + cap.style = "Название таблицы" + except KeyError: + pass + t1 = doc.add_table(rows=3, cols=2) + t1.cell(0, 0).text = "A" + t1.cell(0, 1).text = "B" + t1.cell(1, 0).text = "1" + t1.cell(1, 1).text = "x" + t1.cell(2, 0).text = "2" + t1.cell(2, 1).text = "y" + cont = doc.add_paragraph("Продолжение Таблицы 1") + try: + cont.style = "Название таблицы" + except KeyError: + pass + t2 = doc.add_table(rows=2, cols=2) + t2.cell(0, 0).text = "3" + t2.cell(0, 1).text = "z" + t2.cell(1, 0).text = "4" + t2.cell(1, 1).text = "w" + path = tmp_path / "cont.docx" + doc.save(path) + out = tmp_path / "cont.md" + result = convert_docx(ImportRequest(filename=str(path), output=str(out))) + assert result.ok, result.message + md = out.read_text(encoding="utf-8") + assert md.count("%tbl1") == 1 + assert "Продолжение" not in md + # one header separator only + assert md.count("| --- | --- |") == 1 or md.count("| --- | --- |") == 1 + assert "| 3 | z |" in md + assert "| 4 | w |" in md + assert "| 1 | x |" in md + + +def _ensure_styles(doc: Document) -> None: + from docx.enum.style import WD_STYLE_TYPE + + for name, base in ( + ("Caption Figure", "Caption"), + ("Название таблицы", "Caption"), + ("Caption Listing", "Caption"), + ("Code", "Normal"), + ("Bibliography", "Normal"), + ): + try: + doc.styles[name] + except KeyError: + try: + st = doc.styles.add_style(name, WD_STYLE_TYPE.PARAGRAPH) + try: + st.base_style = doc.styles[base] + except KeyError: + pass + except Exception: + pass + + +def test_convert_minimal_docx(tmp_path: Path): + doc = Document() + _ensure_styles(doc) + doc.add_heading("ВВЕДЕНИЕ", level=1) + # unnumbered look: we'll rely on special title text + p = doc.paragraphs[-1] + p.text = "ВВЕДЕНИЕ" + try: + p.style = doc.styles["Heading 1"] + except KeyError: + pass + doc.add_paragraph("Абзац с жирным текстом и ссылкой [1].") + cap = doc.add_paragraph("Таблица 1 — Демо") + try: + cap.style = "Название таблицы" + except KeyError: + pass + table = doc.add_table(rows=2, cols=2) + table.cell(0, 0).text = "A" + table.cell(0, 1).text = "B" + table.cell(1, 0).text = "1" + table.cell(1, 1).text = "2" + doc.add_heading("ЗАКЛЮЧЕНИЕ", level=1) + doc.paragraphs[-1].text = "ЗАКЛЮЧЕНИЕ" + doc.add_heading("СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ", level=1) + doc.paragraphs[-1].text = "СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ" + bib = doc.add_paragraph("[1]: Иванов И. И. Книга. — М., 2023.") + try: + bib.style = "Bibliography" + except KeyError: + pass + + docx_path = tmp_path / "mini.docx" + doc.save(docx_path) + + result = convert_docx(ImportRequest(filename=str(docx_path), output=str(tmp_path / "mini.md"))) + assert result.ok, result.message + md = Path(result.output_path).read_text(encoding="utf-8") + assert "# *ВВЕДЕНИЕ" in md + assert "%tbl1" in md + assert "| A | B |" in md + assert "[1]:" in md + assert "# *ЗАКЛЮЧЕНИЕ" in md + + +def test_roundtrip_structure(tmp_path: Path): + """MD → DOCX (md2gost) → MD (word2md): keep headings / table / biblio shape.""" + from md2gost.pipeline import ConvertRequest, convert + + md_src = tmp_path / "sample.md" + md_src.write_text( + """# *ВВЕДЕНИЕ + +Текст введения со ссылкой [1]. + +%cmp Сравнение + +| A | B | +|---|---| +| 1 | 2 | + +См. Таблица 1 — будет заменена после нумерации. + +# *ЗАКЛЮЧЕНИЕ + +Итог. + +# *СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ + +[1]: Источник один. — М., 2023. +""", + encoding="utf-8", + ) + docx_out = tmp_path / "sample.docx" + conv = convert( + ConvertRequest( + filename=str(md_src), + output=str(docx_out), + table_continuation="off", + listing_continuation="off", + check=False, + ) + ) + assert conv.ok, conv.message + + md2 = tmp_path / "back.md" + imp = convert_docx(ImportRequest(filename=str(docx_out), output=str(md2))) + assert imp.ok, imp.message + text = md2.read_text(encoding="utf-8") + assert "# *ВВЕДЕНИЕ" in text + assert "# *ЗАКЛЮЧЕНИЕ" in text + assert "| A | B |" in text or "| A |" in text + assert "[1]" in text + assert "%tbl" in text or "Таблица" in text or "|" in text + + +def test_table_grid_merge_markers(): + doc = Document() + table = doc.add_table(rows=2, cols=2) + table.cell(0, 0).text = "X" + table.cell(0, 1).text = "Y" + table.cell(1, 0).text = "Z" + table.cell(1, 1).text = "W" + # merge horizontally first row + table.cell(0, 0).merge(table.cell(0, 1)) + rows, merges = table_to_grid(table) + assert len(rows) == 2 + # first row should have continue marker for span + assert any(cell == ">" for cell in rows[0]) or merges[0][1][1] == "continue" + + +def test_nobreak_hyphen_preserved(tmp_path: Path): + """md2gost writes '-' as w:noBreakHyphen; word2md must restore ASCII hyphen.""" + from md2gost.pipeline import ConvertRequest, convert + from md2gost.util import create_element + from docx.oxml.ns import qn + + md_src = tmp_path / "hyphen.md" + md_src.write_text( + "# *ВВЕДЕНИЕ\n\n" + "Во-первых, ИТ-компания и 63-ФЗ. Организационно-правовая форма.\n", + encoding="utf-8", + ) + docx_out = tmp_path / "hyphen.docx" + conv = convert( + ConvertRequest( + filename=str(md_src), + output=str(docx_out), + table_continuation="off", + listing_continuation="off", + ) + ) + assert conv.ok, conv.message + + # Sanity: DOCX really has noBreakHyphen + doc = Document(str(docx_out)) + xml = doc.element.body.xml + assert "noBreakHyphen" in xml + # python-docx .text drops them + joined = " ".join(p.text or "" for p in doc.paragraphs) + assert "Вопервых" in joined.replace(" ", "") or "Во-первых" not in joined + + md2 = tmp_path / "back.md" + imp = convert_docx(ImportRequest(filename=str(docx_out), output=str(md2))) + assert imp.ok, imp.message + text = md2.read_text(encoding="utf-8") + assert "Во-первых" in text + assert "ИТ-компания" in text + assert "63-ФЗ" in text + assert "Организационно-правовая" in text diff --git a/tests/test_word_fix.py b/tests/test_word_fix.py index adfce33..cd7794f 100644 --- a/tests/test_word_fix.py +++ b/tests/test_word_fix.py @@ -13,7 +13,10 @@ from md2gost.word_fix import ( continuation_label, find_page_break_row, fix_continuations, + is_caption_orphaned, + is_orphan_header_break, parse_caption_text, + split_vmerge_spans_at_break, ) @@ -54,6 +57,33 @@ def test_find_page_break_row(): assert find_page_break_row([1, 1, 2, 2]) == 3 assert find_page_break_row([2, 2, 3]) == 3 assert find_page_break_row([1, 2]) == 2 + # typical vMerge table pages after cell-based detection + assert find_page_break_row([14, 14, 14, 14, 14, 14, 15, 15]) == 7 + + +def test_orphan_header_and_caption_guards(): + """Method guide: no title-only / title+column-headers alone at page end.""" + # Split would leave only heading row on previous page + assert is_orphan_header_break(2, had_header=True) is True + assert is_orphan_header_break(2, had_header=False) is False + assert is_orphan_header_break(3, had_header=True) is False + assert is_orphan_header_break(None, had_header=True) is False + # Caption on earlier page than first table row + assert is_caption_orphaned(5, 6) is True + assert is_caption_orphaned(5, 5) is False + assert is_caption_orphaned(None, 6) is False + + +def test_split_vmerge_spans_at_break(): + # (row, col, span, text) — merge rows 2..6 (span 5), break at row 7 → all in first + spans = [(2, 1, 5, "A"), (7, 1, 2, "B")] + first, cont = split_vmerge_spans_at_break(spans, 7) + assert first == [(2, 1, 5, "A")] + assert cont == [(1, 1, 2, "B")] + # merge crosses break: rows 5..8, break at 7 + first, cont = split_vmerge_spans_at_break([(5, 1, 4, "X")], 7) + assert first == [(5, 1, 2, "X")] + assert cont == [(1, 1, 2, "X")] def test_continuation_label(): diff --git a/todo.txt b/todo.txt index ed00b42..bc3bbe5 100644 --- a/todo.txt +++ b/todo.txt @@ -1,2 +1,2 @@ -- Ожидаем внедрение BPMN в mermaid -- Оценки с текстом через сам Word а не предугадываание \ No newline at end of file +- Оценки с текстом через сам Word а не предугадываание +- Проверка «текст могут не принять»: эвристика отчёта ИИ (пишет про сдачу ТЗ, а не про предмет). `--check` + отдельный разбор по абзацам (`--check-voice` / кнопка в GUI). Идеи правил — сравнение живой практики и сгенерированного отчёта: телеология «последующие работы», преамбулы разделов, заключение-чеклист, колонки «значение для объекта», плотность ссылок, консультантский лоск. diff --git a/uv.lock b/uv.lock index 370420d..3c5fe30 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,6 @@ version = 1 revision = 3 -requires-python = ">=3.10" +requires-python = ">=3.11" [[package]] name = "babel" @@ -26,22 +26,6 @@ version = "3.5.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e5/3f/143b048436775b0f76ac3eec145c019e8173ccc2885c8f20319b996d5e83/charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3", size = 171764, upload-time = "2026-08-15T08:20:44.807Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/71/aa/554e2614f38fc34c58ff1d0911ae8535ad2516440d5482d76fe59f1088b0/charset_normalizer-3.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1ee1e296209fdce05b81b663250eefa02213a2da7b41bf26f7829b8ba3545aa", size = 369072, upload-time = "2026-08-15T08:16:22.964Z" }, - { url = "https://files.pythonhosted.org/packages/03/6d/439231dfc3ccfa6f8c06477b7da2219cbd41a2de3d49084df8ec7b5100f2/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9fbdce1e47394b09bc9f26ab117dfc8d6491977a11d86f592bb42c779db2fda", size = 251142, upload-time = "2026-08-15T08:16:24.81Z" }, - { url = "https://files.pythonhosted.org/packages/55/53/7d819bd23a00ef45039146fa2cce1daa2f0771e758c5653ee1f6edac91ed/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:00668ebb0609751758682eb0b5857e7c35b9f00e84dfdef062e103244ec94d45", size = 240714, upload-time = "2026-08-15T08:16:26.392Z" }, - { url = "https://files.pythonhosted.org/packages/b2/2c/45847198c16f4b38090cc7423b2b6a9008e438704d8ab413211832498d31/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba2f37ee79e6338845261a3c5b1784e5d1acdff2c0785b284f1b633033d136ab", size = 279637, upload-time = "2026-08-15T08:16:27.961Z" }, - { url = "https://files.pythonhosted.org/packages/69/2b/d8be3523ddf9f0b0f3e56d1359034aa10653a4d11564c697f802b4775766/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ce854f5f478050ade5a238731c4ca985a7d3b3cb53ff600a9b5c3b689b5f0a7a", size = 276543, upload-time = "2026-08-15T08:16:29.399Z" }, - { url = "https://files.pythonhosted.org/packages/32/cd/4f564b8f132de25db594efc706897069f016790cea63a5669c9df2675f64/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96eefc178f8636b9c760c5829345307fd81cfae9ab1e80997dbddeb0f54ee9a3", size = 261644, upload-time = "2026-08-15T08:16:30.722Z" }, - { url = "https://files.pythonhosted.org/packages/f5/e3/38b975422534a608f98c360e79c2f07c763d66dd4272300d45fb1fee54b0/charset_normalizer-3.5.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:366ec70f5547c640d3ce1985722490f23faf4eb5216a7eeba78277490e78dacb", size = 259609, upload-time = "2026-08-15T08:16:32.248Z" }, - { url = "https://files.pythonhosted.org/packages/87/bd/fbc24d825c66f1c74f6ccdea3742c3d8354a4888e86d1315a197fee69061/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:950f23cb393f85543777b0433f082cddd25b51ab398eac7971146495679efe5f", size = 252457, upload-time = "2026-08-15T08:16:33.849Z" }, - { url = "https://files.pythonhosted.org/packages/b9/2d/918d0e98a0e679469ed05bb2d90c2088b4d315bb612969d8499f76fb5210/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1dcc36dcb96abc02236e182d17e0f71430152a6c2c7447421da2d2dc144edea", size = 242240, upload-time = "2026-08-15T08:16:35.396Z" }, - { url = "https://files.pythonhosted.org/packages/20/c8/c36f6e0b2dfec351bd38cbc05362697e58bcd073d7dbd95154290c9714ce/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:07ffd07412fc5d5e84cd8952acf9ff7e4ed7a708e69d1bada19d8ba91711353f", size = 280308, upload-time = "2026-08-15T08:16:36.825Z" }, - { url = "https://files.pythonhosted.org/packages/ca/7b/311b3e02e8c4092400c449c850a760d8c45d900983c83a70cc07208c551d/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f5542f9b941279d82d41eb0aa9f98eba36fe4df5c7086c651df7944935b37182", size = 258679, upload-time = "2026-08-15T08:16:38.22Z" }, - { url = "https://files.pythonhosted.org/packages/b9/90/082cc45599c392f28c036a497f49e0634041a785fc3849c80ccf396d096f/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a545775cfe815855ea32d7c27731d79da358ef2055b4a25830231b1622dd18aa", size = 277221, upload-time = "2026-08-15T08:16:39.62Z" }, - { url = "https://files.pythonhosted.org/packages/58/ad/b9aecf38d805cbcf84fa94f14c5d972a16561e20296a11dc799a5dcf3763/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:494b70049a4d69aec6e8137c13af4cf8db8c9f9820a1392ac293b0dd2987a818", size = 263799, upload-time = "2026-08-15T08:16:40.885Z" }, - { url = "https://files.pythonhosted.org/packages/b7/23/b38a20598d5a825f85d9d7636860e56ff0db1479f86497a6e485aa9326f7/charset_normalizer-3.5.1-cp310-cp310-win32.whl", hash = "sha256:94fbf1c0c6cc0d3d5e50f9a9313a8cdca90dd696d34b381cd1704f8c9e939f20", size = 182037, upload-time = "2026-08-15T08:16:42.198Z" }, - { url = "https://files.pythonhosted.org/packages/d2/21/83fffb77864408b8bf0fe1ca603926401d6f8775a8e150b39aacc9958f8a/charset_normalizer-3.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:be47f99644b208bff7766314013f9acf57b056b04191d570d68ad14022cf5b1d", size = 206030, upload-time = "2026-08-15T08:16:43.787Z" }, - { url = "https://files.pythonhosted.org/packages/86/2e/b93135b5034b1157fb29554b0d06d4844ce62282f0e0a14036f93d7ee2e7/charset_normalizer-3.5.1-cp310-cp310-win_arm64.whl", hash = "sha256:a6d095662e73e74f0a49988e0593373e243e3a52e27bfeea0a859e88acf4a0f5", size = 185092, upload-time = "2026-08-15T08:16:45.177Z" }, { url = "https://files.pythonhosted.org/packages/6a/b6/034f6802e9c3f6418966cfabb7db8c9252cc2429c5098f41cc43af804149/charset_normalizer-3.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eda059b6bc8bc0812d626fd91a7ce01bf583df0a61296eff390fd94141a34e30", size = 363585, upload-time = "2026-08-15T08:16:46.646Z" }, { url = "https://files.pythonhosted.org/packages/d5/fa/6a7e2a7c4b5451912b8c417732df79574354443592a88d616de03da66ae5/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa2bb0b37202dca27175591f761108b5d34096ade1191ffe4808bdf6b1571488", size = 251189, upload-time = "2026-08-15T08:16:48.287Z" }, { url = "https://files.pythonhosted.org/packages/a4/c8/ab42b07cfd82e919f427fcfaa7c41abae8242833ad1aad66d42bae40b669/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b2b1b3fa5670c127b246df1d0c059defd41f689a868a3b9d79df9b1cac42d22", size = 239724, upload-time = "2026-08-15T08:16:49.67Z" }, @@ -192,6 +176,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "data-flow-diagram" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/90/cd847322671c58555a0de75a53869e0650f217d4f9fe415532a0a8ec1587/data_flow_diagram-1.17.0.tar.gz", hash = "sha256:6856f009fdaaede077c8c2181ab22d3967ee9d908fce7e7e867afb894bb3c8dc", size = 33331, upload-time = "2026-09-03T16:00:42.656Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/fd/988b2c4ca0dba660ccf021bf732ae3b37e7a5d85bdf0182f1f0225a78571/data_flow_diagram-1.17.0-py3-none-any.whl", hash = "sha256:60dc1bb98b4249383b262bd71f418202c3b95f8d7b316a205747883e99eafaf1", size = 32213, upload-time = "2026-09-03T16:00:41.267Z" }, +] + [[package]] name = "docxcompose" version = "1.4.1" @@ -207,18 +200,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0f/03/053f291ee7ad6a3fd5a20eef3ff73925fe815803335180808582719f2c06/docxcompose-1.4.1-py3-none-any.whl", hash = "sha256:8a0a5c6f5bc8f4c0b2691d9714aac7195c05229c24d2dffc2222b5bdec547258", size = 24120, upload-time = "2026-03-14T17:42:50.667Z" }, ] -[[package]] -name = "exceptiongroup" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, -] - [[package]] name = "freetype-py" version = "2.5.1" @@ -239,15 +220,6 @@ version = "3.5.5" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0b/d8/7cc97c142388aef03f622e001c572c4f84e9252a439549d483f555771970/greenlet-3.5.5.tar.gz", hash = "sha256:adb4bae02e91a8e863e48b177e4014bdcac8a6b5e047ea1df687a61534b85e6c", size = 207585, upload-time = "2026-08-10T15:09:36.136Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/67/07ecd6d85c0f253363cbc4ac9e7dd048ca571a267fbccfea084d6009ac3b/greenlet-3.5.5-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:816230f469381ad0a43abc9fa8dda5a699e32fb78958dde32ded93213b70a667", size = 292971, upload-time = "2026-08-10T13:28:09.837Z" }, - { url = "https://files.pythonhosted.org/packages/24/35/426733bc24247ee17bb76df90f550c299ff5f8572b2bdd7cacb5c53fd994/greenlet-3.5.5-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5433cf291e0ef9114bd14d0d824db6e5e4a43033234bca48181a9597acca07b", size = 609290, upload-time = "2026-08-10T14:14:32.273Z" }, - { url = "https://files.pythonhosted.org/packages/9c/dc/17d3a5acceb2fd0bbc9682a228117b719cdfb68085e6dbb33fa325755f27/greenlet-3.5.5-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:19d59f068887d8c5907fc177f27683413ace3011b6ed646c0b309266e74a6502", size = 622650, upload-time = "2026-08-10T14:27:22.004Z" }, - { url = "https://files.pythonhosted.org/packages/74/3f/b31e6bd6adb8f80492efb6a47e8f9cd0e7335db5aac2795b1876d5afcfee/greenlet-3.5.5-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:86c5113d698cb8d927b2750bb1f1d59eefe3a37e0e0217491aee29a7f84ef52c", size = 629560, upload-time = "2026-08-10T14:30:04.733Z" }, - { url = "https://files.pythonhosted.org/packages/51/91/00b3c0566316c6f383ea16ee05388ec4f57f6e0afa49e72ae471eda8c47f/greenlet-3.5.5-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff00e12102358292087274dfb1669132387ff6e7920ebf9d85f4826ce0d3a56", size = 622819, upload-time = "2026-08-10T13:40:46.562Z" }, - { url = "https://files.pythonhosted.org/packages/18/5e/58c561efde575c4ca070030e2e0513e8d1b07b76d8a2d84a9bcef1becd42/greenlet-3.5.5-cp310-cp310-manylinux_2_39_riscv64.whl", hash = "sha256:c69bed34470abfcd456984fdadaa18e62169af4480335c45f3c32d1d9c12e638", size = 425489, upload-time = "2026-08-10T14:29:59.768Z" }, - { url = "https://files.pythonhosted.org/packages/3c/78/52de4f7ac9152ad1dbc8f437895c1e573d30ee1e1427e0f91a280e2417b6/greenlet-3.5.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:523bb8e27614d77101ea7a8cf59f8d91219b72d5c29f6a038c92b50828bfa8d0", size = 1582164, upload-time = "2026-08-10T14:15:02.645Z" }, - { url = "https://files.pythonhosted.org/packages/f6/ff/c3855a00c2417e8f61c7b9f0bc4f8f599c6928f5c15681ad251e78a91e05/greenlet-3.5.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f1e2db190db51c17433eee424803818cf0670bf049d9cfe0dd07be111d1aa7c4", size = 1648807, upload-time = "2026-08-10T13:40:27.471Z" }, - { url = "https://files.pythonhosted.org/packages/70/21/d29ff6a79dbd1ec1fe74c155d25d4c5a85e221d6b9ffc2cc7a709e7c8c39/greenlet-3.5.5-cp310-cp310-win_amd64.whl", hash = "sha256:740e544169527b82695ce76af2f7ad6f030904658f2f3921a1d245771fb88cfc", size = 322832, upload-time = "2026-08-10T13:28:10.85Z" }, { url = "https://files.pythonhosted.org/packages/4e/a3/07297917485ee2ca85bc3c8dc6ed85ad3fffcf424047fba62671dba68e97/greenlet-3.5.5-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:be63afcbbccfad3dd95a1ba12ada84dab2ef32031973d80b5b92df67fa763a61", size = 294165, upload-time = "2026-08-10T13:25:17.987Z" }, { url = "https://files.pythonhosted.org/packages/db/51/6f732f9314cda54c5fd48a7620c7160f4f286967e8045ad94b9d66ce80b7/greenlet-3.5.5-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a268024ce2d7d2b04694bf1594058981a9fa663d1df4b762dee499211ed7c1c", size = 613610, upload-time = "2026-08-10T14:14:33.829Z" }, { url = "https://files.pythonhosted.org/packages/d8/c0/b27589e25d220289edcd4d582b2b17b83058d1a56d53d971b6ea1a34f10d/greenlet-3.5.5-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35cbb8bf55ace57fbccb4fb8622c4521713acd8691e77f4696d416ea7ca527da", size = 625481, upload-time = "2026-08-10T14:27:23.647Z" }, @@ -352,23 +324,6 @@ version = "5.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/76/3d/14e82fc7c8fb1b7761f7e748fd47e2ec8276d137b6acfe5a4bb73853e08f/lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd", size = 3679479, upload-time = "2025-04-23T01:50:29.322Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/1f/a3b6b74a451ceb84b471caa75c934d2430a4d84395d38ef201d539f38cd1/lxml-5.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e7bc6df34d42322c5289e37e9971d6ed114e3776b45fa879f734bded9d1fea9c", size = 8076838, upload-time = "2025-04-23T01:44:29.325Z" }, - { url = "https://files.pythonhosted.org/packages/36/af/a567a55b3e47135b4d1f05a1118c24529104c003f95851374b3748139dc1/lxml-5.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6854f8bd8a1536f8a1d9a3655e6354faa6406621cf857dc27b681b69860645c7", size = 4381827, upload-time = "2025-04-23T01:44:33.345Z" }, - { url = "https://files.pythonhosted.org/packages/50/ba/4ee47d24c675932b3eb5b6de77d0f623c2db6dc466e7a1f199792c5e3e3a/lxml-5.4.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:696ea9e87442467819ac22394ca36cb3d01848dad1be6fac3fb612d3bd5a12cf", size = 5204098, upload-time = "2025-04-23T01:44:35.809Z" }, - { url = "https://files.pythonhosted.org/packages/f2/0f/b4db6dfebfefe3abafe360f42a3d471881687fd449a0b86b70f1f2683438/lxml-5.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ef80aeac414f33c24b3815ecd560cee272786c3adfa5f31316d8b349bfade28", size = 4930261, upload-time = "2025-04-23T01:44:38.271Z" }, - { url = "https://files.pythonhosted.org/packages/0b/1f/0bb1bae1ce056910f8db81c6aba80fec0e46c98d77c0f59298c70cd362a3/lxml-5.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b9c2754cef6963f3408ab381ea55f47dabc6f78f4b8ebb0f0b25cf1ac1f7609", size = 5529621, upload-time = "2025-04-23T01:44:40.921Z" }, - { url = "https://files.pythonhosted.org/packages/21/f5/e7b66a533fc4a1e7fa63dd22a1ab2ec4d10319b909211181e1ab3e539295/lxml-5.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7a62cc23d754bb449d63ff35334acc9f5c02e6dae830d78dab4dd12b78a524f4", size = 4983231, upload-time = "2025-04-23T01:44:43.871Z" }, - { url = "https://files.pythonhosted.org/packages/11/39/a38244b669c2d95a6a101a84d3c85ba921fea827e9e5483e93168bf1ccb2/lxml-5.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f82125bc7203c5ae8633a7d5d20bcfdff0ba33e436e4ab0abc026a53a8960b7", size = 5084279, upload-time = "2025-04-23T01:44:46.632Z" }, - { url = "https://files.pythonhosted.org/packages/db/64/48cac242347a09a07740d6cee7b7fd4663d5c1abd65f2e3c60420e231b27/lxml-5.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b67319b4aef1a6c56576ff544b67a2a6fbd7eaee485b241cabf53115e8908b8f", size = 4927405, upload-time = "2025-04-23T01:44:49.843Z" }, - { url = "https://files.pythonhosted.org/packages/98/89/97442835fbb01d80b72374f9594fe44f01817d203fa056e9906128a5d896/lxml-5.4.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:a8ef956fce64c8551221f395ba21d0724fed6b9b6242ca4f2f7beb4ce2f41997", size = 5550169, upload-time = "2025-04-23T01:44:52.791Z" }, - { url = "https://files.pythonhosted.org/packages/f1/97/164ca398ee654eb21f29c6b582685c6c6b9d62d5213abc9b8380278e9c0a/lxml-5.4.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:0a01ce7d8479dce84fc03324e3b0c9c90b1ece9a9bb6a1b6c9025e7e4520e78c", size = 5062691, upload-time = "2025-04-23T01:44:56.108Z" }, - { url = "https://files.pythonhosted.org/packages/d0/bc/712b96823d7feb53482d2e4f59c090fb18ec7b0d0b476f353b3085893cda/lxml-5.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:91505d3ddebf268bb1588eb0f63821f738d20e1e7f05d3c647a5ca900288760b", size = 5133503, upload-time = "2025-04-23T01:44:59.222Z" }, - { url = "https://files.pythonhosted.org/packages/d4/55/a62a39e8f9da2a8b6002603475e3c57c870cd9c95fd4b94d4d9ac9036055/lxml-5.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a3bcdde35d82ff385f4ede021df801b5c4a5bcdfb61ea87caabcebfc4945dc1b", size = 4999346, upload-time = "2025-04-23T01:45:02.088Z" }, - { url = "https://files.pythonhosted.org/packages/ea/47/a393728ae001b92bb1a9e095e570bf71ec7f7fbae7688a4792222e56e5b9/lxml-5.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aea7c06667b987787c7d1f5e1dfcd70419b711cdb47d6b4bb4ad4b76777a0563", size = 5627139, upload-time = "2025-04-23T01:45:04.582Z" }, - { url = "https://files.pythonhosted.org/packages/5e/5f/9dcaaad037c3e642a7ea64b479aa082968de46dd67a8293c541742b6c9db/lxml-5.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a7fb111eef4d05909b82152721a59c1b14d0f365e2be4c742a473c5d7372f4f5", size = 5465609, upload-time = "2025-04-23T01:45:07.649Z" }, - { url = "https://files.pythonhosted.org/packages/a7/0a/ebcae89edf27e61c45023005171d0ba95cb414ee41c045ae4caf1b8487fd/lxml-5.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:43d549b876ce64aa18b2328faff70f5877f8c6dede415f80a2f799d31644d776", size = 5192285, upload-time = "2025-04-23T01:45:10.456Z" }, - { url = "https://files.pythonhosted.org/packages/42/ad/cc8140ca99add7d85c92db8b2354638ed6d5cc0e917b21d36039cb15a238/lxml-5.4.0-cp310-cp310-win32.whl", hash = "sha256:75133890e40d229d6c5837b0312abbe5bac1c342452cf0e12523477cd3aa21e7", size = 3477507, upload-time = "2025-04-23T01:45:12.474Z" }, - { url = "https://files.pythonhosted.org/packages/e9/39/597ce090da1097d2aabd2f9ef42187a6c9c8546d67c419ce61b88b336c85/lxml-5.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:de5b4e1088523e2b6f730d0509a9a813355b7f5659d70eb4f319c76beea2e250", size = 3805104, upload-time = "2025-04-23T01:45:15.104Z" }, { url = "https://files.pythonhosted.org/packages/81/2d/67693cc8a605a12e5975380d7ff83020dcc759351b5a066e1cced04f797b/lxml-5.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:98a3912194c079ef37e716ed228ae0dcb960992100461b704aea4e93af6b0bb9", size = 8083240, upload-time = "2025-04-23T01:45:18.566Z" }, { url = "https://files.pythonhosted.org/packages/73/53/b5a05ab300a808b72e848efd152fe9c022c0181b0a70b8bca1199f1bed26/lxml-5.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ea0252b51d296a75f6118ed0d8696888e7403408ad42345d7dfd0d1e93309a7", size = 4387685, upload-time = "2025-04-23T01:45:21.387Z" }, { url = "https://files.pythonhosted.org/packages/d8/cb/1a3879c5f512bdcd32995c301886fe082b2edd83c87d41b6d42d89b4ea4d/lxml-5.4.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92b69441d1bd39f4940f9eadfa417a25862242ca2c396b406f9272ef09cdcaa", size = 4991164, upload-time = "2025-04-23T01:45:23.849Z" }, @@ -420,12 +375,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/cd/95fa2201041a610c4d08ddaf31d43b98ecc4b1d74b1e7245b1abdab443cb/lxml-5.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:15a665ad90054a3d4f397bc40f73948d48e36e4c09f9bcffc7d90c87410e478a", size = 5021569, upload-time = "2025-04-23T01:47:33.805Z" }, { url = "https://files.pythonhosted.org/packages/2d/a6/31da006fead660b9512d08d23d31e93ad3477dd47cc42e3285f143443176/lxml-5.4.0-cp313-cp313-win32.whl", hash = "sha256:d5663bc1b471c79f5c833cffbc9b87d7bf13f87e055a5c86c363ccd2348d7e82", size = 3485270, upload-time = "2025-04-23T01:47:36.133Z" }, { url = "https://files.pythonhosted.org/packages/fc/14/c115516c62a7d2499781d2d3d7215218c0731b2c940753bf9f9b7b73924d/lxml-5.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:bcb7a1096b4b6b24ce1ac24d4942ad98f983cd3810f9711bcd0293f43a9d8b9f", size = 3814606, upload-time = "2025-04-23T01:47:39.028Z" }, - { url = "https://files.pythonhosted.org/packages/c6/b0/e4d1cbb8c078bc4ae44de9c6a79fec4e2b4151b1b4d50af71d799e76b177/lxml-5.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1b717b00a71b901b4667226bba282dd462c42ccf618ade12f9ba3674e1fabc55", size = 3892319, upload-time = "2025-04-23T01:49:22.069Z" }, - { url = "https://files.pythonhosted.org/packages/5b/aa/e2bdefba40d815059bcb60b371a36fbfcce970a935370e1b367ba1cc8f74/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27a9ded0f0b52098ff89dd4c418325b987feed2ea5cc86e8860b0f844285d740", size = 4211614, upload-time = "2025-04-23T01:49:24.599Z" }, - { url = "https://files.pythonhosted.org/packages/3c/5f/91ff89d1e092e7cfdd8453a939436ac116db0a665e7f4be0cd8e65c7dc5a/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b7ce10634113651d6f383aa712a194179dcd496bd8c41e191cec2099fa09de5", size = 4306273, upload-time = "2025-04-23T01:49:27.355Z" }, - { url = "https://files.pythonhosted.org/packages/be/7c/8c3f15df2ca534589717bfd19d1e3482167801caedfa4d90a575facf68a6/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53370c26500d22b45182f98847243efb518d268374a9570409d2e2276232fd37", size = 4208552, upload-time = "2025-04-23T01:49:29.949Z" }, - { url = "https://files.pythonhosted.org/packages/7d/d8/9567afb1665f64d73fc54eb904e418d1138d7f011ed00647121b4dd60b38/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c6364038c519dffdbe07e3cf42e6a7f8b90c275d4d1617a69bb59734c1a2d571", size = 4331091, upload-time = "2025-04-23T01:49:32.842Z" }, - { url = "https://files.pythonhosted.org/packages/f1/ab/fdbbd91d8d82bf1a723ba88ec3e3d76c022b53c391b0c13cad441cdb8f9e/lxml-5.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b12cb6527599808ada9eb2cd6e0e7d3d8f13fe7bbb01c6311255a15ded4c7ab4", size = 3487862, upload-time = "2025-04-23T01:49:36.296Z" }, ] [[package]] @@ -442,6 +391,7 @@ name = "md2gost" version = "0.1.0" source = { editable = "." } dependencies = [ + { name = "data-flow-diagram" }, { name = "docxcompose" }, { name = "freetype-py" }, { name = "latex2mathml" }, @@ -467,6 +417,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "data-flow-diagram", specifier = ">=1.16.0,<2" }, { name = "docxcompose", specifier = ">=1.4.0,<2" }, { name = "freetype-py", specifier = ">=2.4.0,<3" }, { name = "latex2mathml", specifier = ">=3.76.0,<4" }, @@ -513,17 +464,6 @@ version = "10.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/cd/74/ad3d526f3bf7b6d3f408b73fde271ec69dfac8b81341a318ce825f2b3812/pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06", size = 46555059, upload-time = "2024-07-01T09:48:43.583Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/69/a31cccd538ca0b5272be2a38347f8839b97a14be104ea08b0db92f749c74/pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e", size = 3509271, upload-time = "2024-07-01T09:45:22.07Z" }, - { url = "https://files.pythonhosted.org/packages/9a/9e/4143b907be8ea0bce215f2ae4f7480027473f8b61fcedfda9d851082a5d2/pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d", size = 3375658, upload-time = "2024-07-01T09:45:25.292Z" }, - { url = "https://files.pythonhosted.org/packages/8a/25/1fc45761955f9359b1169aa75e241551e74ac01a09f487adaaf4c3472d11/pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856", size = 4332075, upload-time = "2024-07-01T09:45:27.94Z" }, - { url = "https://files.pythonhosted.org/packages/5e/dd/425b95d0151e1d6c951f45051112394f130df3da67363b6bc75dc4c27aba/pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f", size = 4444808, upload-time = "2024-07-01T09:45:30.305Z" }, - { url = "https://files.pythonhosted.org/packages/b1/84/9a15cc5726cbbfe7f9f90bfb11f5d028586595907cd093815ca6644932e3/pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b", size = 4356290, upload-time = "2024-07-01T09:45:32.868Z" }, - { url = "https://files.pythonhosted.org/packages/b5/5b/6651c288b08df3b8c1e2f8c1152201e0b25d240e22ddade0f1e242fc9fa0/pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc", size = 4525163, upload-time = "2024-07-01T09:45:35.279Z" }, - { url = "https://files.pythonhosted.org/packages/07/8b/34854bf11a83c248505c8cb0fcf8d3d0b459a2246c8809b967963b6b12ae/pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e", size = 4463100, upload-time = "2024-07-01T09:45:37.74Z" }, - { url = "https://files.pythonhosted.org/packages/78/63/0632aee4e82476d9cbe5200c0cdf9ba41ee04ed77887432845264d81116d/pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46", size = 4592880, upload-time = "2024-07-01T09:45:39.89Z" }, - { url = "https://files.pythonhosted.org/packages/df/56/b8663d7520671b4398b9d97e1ed9f583d4afcbefbda3c6188325e8c297bd/pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984", size = 2235218, upload-time = "2024-07-01T09:45:42.771Z" }, - { url = "https://files.pythonhosted.org/packages/f4/72/0203e94a91ddb4a9d5238434ae6c1ca10e610e8487036132ea9bf806ca2a/pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141", size = 2554487, upload-time = "2024-07-01T09:45:45.176Z" }, - { url = "https://files.pythonhosted.org/packages/bd/52/7e7e93d7a6e4290543f17dc6f7d3af4bd0b3dd9926e2e8a35ac2282bc5f4/pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1", size = 2243219, upload-time = "2024-07-01T09:45:47.274Z" }, { url = "https://files.pythonhosted.org/packages/a7/62/c9449f9c3043c37f73e7487ec4ef0c03eb9c9afc91a92b977a67b3c0bbc5/pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c", size = 3509265, upload-time = "2024-07-01T09:45:49.812Z" }, { url = "https://files.pythonhosted.org/packages/f4/5f/491dafc7bbf5a3cc1845dc0430872e8096eb9e2b6f8161509d124594ec2d/pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be", size = 3375655, upload-time = "2024-07-01T09:45:52.462Z" }, { url = "https://files.pythonhosted.org/packages/73/d5/c4011a76f4207a3c151134cd22a1415741e42fa5ddecec7c0182887deb3d/pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3", size = 4340304, upload-time = "2024-07-01T09:45:55.006Z" }, @@ -557,13 +497,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/f9/cfaa5082ca9bc4a6de66ffe1c12c2d90bf09c309a5f52b27759a596900e7/pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060", size = 2235603, upload-time = "2024-07-01T09:47:03.918Z" }, { url = "https://files.pythonhosted.org/packages/01/6a/30ff0eef6e0c0e71e55ded56a38d4859bf9d3634a94a88743897b5f96936/pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea", size = 2554972, upload-time = "2024-07-01T09:47:06.152Z" }, { url = "https://files.pythonhosted.org/packages/48/2c/2e0a52890f269435eee38b21c8218e102c621fe8d8df8b9dd06fabf879ba/pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d", size = 2243375, upload-time = "2024-07-01T09:47:09.065Z" }, - { url = "https://files.pythonhosted.org/packages/38/30/095d4f55f3a053392f75e2eae45eba3228452783bab3d9a920b951ac495c/pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4", size = 3493889, upload-time = "2024-07-01T09:48:04.815Z" }, - { url = "https://files.pythonhosted.org/packages/f3/e8/4ff79788803a5fcd5dc35efdc9386af153569853767bff74540725b45863/pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da", size = 3346160, upload-time = "2024-07-01T09:48:07.206Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ac/4184edd511b14f760c73f5bb8a5d6fd85c591c8aff7c2229677a355c4179/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026", size = 3435020, upload-time = "2024-07-01T09:48:09.66Z" }, - { url = "https://files.pythonhosted.org/packages/da/21/1749cd09160149c0a246a81d646e05f35041619ce76f6493d6a96e8d1103/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e", size = 3490539, upload-time = "2024-07-01T09:48:12.529Z" }, - { url = "https://files.pythonhosted.org/packages/b6/f5/f71fe1888b96083b3f6dfa0709101f61fc9e972c0c8d04e9d93ccef2a045/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5", size = 3476125, upload-time = "2024-07-01T09:48:14.891Z" }, - { url = "https://files.pythonhosted.org/packages/96/b9/c0362c54290a31866c3526848583a2f45a535aa9d725fd31e25d318c805f/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885", size = 3579373, upload-time = "2024-07-01T09:48:17.601Z" }, - { url = "https://files.pythonhosted.org/packages/52/3b/ce7a01026a7cf46e5452afa86f97a5e88ca97f562cafa76570178ab56d8d/pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5", size = 2554661, upload-time = "2024-07-01T09:48:20.293Z" }, ] [[package]] @@ -621,12 +554,10 @@ version = "9.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ @@ -647,9 +578,6 @@ name = "pywin32" version = "312" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/1b/9cfdeac80ee45bebbbcb31f1b7b99a0d81a1c72de48d837be984e0e88b1d/pywin32-312-cp310-cp310-win32.whl", hash = "sha256:772235332b5d1024c696f11cea1ae4be7930f0a8b894bb43db14e3f435f1ff7e", size = 6361387, upload-time = "2026-06-04T07:49:14.329Z" }, - { url = "https://files.pythonhosted.org/packages/33/b1/7afc96d041d982c27bc2df6f853d43f01fd273e3d39d04be3647ddeb533d/pywin32-312-cp310-cp310-win_amd64.whl", hash = "sha256:5dbc35d2b5320dc07f25fa31269cfb767471002b17de5eb067d03da68c7cb2db", size = 6926780, upload-time = "2026-06-04T07:49:16.881Z" }, - { url = "https://files.pythonhosted.org/packages/ce/3a/4140da9ad54108e517f4a16b2d83da3033e08662144623e1239587cb7db6/pywin32-312-cp310-cp310-win_arm64.whl", hash = "sha256:3020656e34f1cf7faeb7bccd2b84653a607c6ff0c55ada85e6487d61716deabd", size = 4307203, upload-time = "2026-06-04T07:49:18.993Z" }, { url = "https://files.pythonhosted.org/packages/1f/f5/10a6e845a00fc5e7afd0a988b744f403d4d57162a28d160a093c4d9322f0/pywin32-312-cp311-cp311-win32.whl", hash = "sha256:17948aeadbdb091f0ced6ef0841620794e68327b94ee415571c1203594b7215c", size = 6362659, upload-time = "2026-06-04T07:49:21.349Z" }, { url = "https://files.pythonhosted.org/packages/35/c4/dcd2d62b5944b6d5db53413a5899016ccd57ffcb7278f3f81655d25d2027/pywin32-312-cp311-cp311-win_amd64.whl", hash = "sha256:d11417d84412f859b722fad0841b3614459ed0047f7542d8362e77884f6b6e8a", size = 6928825, upload-time = "2026-06-04T07:49:23.934Z" }, { url = "https://files.pythonhosted.org/packages/b7/56/3cbb433fe4501cdba2eb9040f56a4e1a8243faa4186b25295564d1a7a79d/pywin32-312-cp311-cp311-win_arm64.whl", hash = "sha256:b2200a054ca6d6625c4842fc56a4976a4b47f96b73dbe5538c3f813a80359f47", size = 6721875, upload-time = "2026-06-04T07:49:26.416Z" }, @@ -682,11 +610,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9d/dc/12f6b77802a4b5a56e3f03756d21cc9247f3cd6399ccd3c51b2753d72aa5/quickjs_ng-0.16.2.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:c131d2fb5c9741ad2da9475807453dc98a31fde1df5f096e4037b1a7ae576025", size = 2527814, upload-time = "2026-09-01T12:00:03.318Z" }, { url = "https://files.pythonhosted.org/packages/d2/c0/3d50214bb88361541712d15da7db0f291daa01294c540fc9f5f0d602c750/quickjs_ng-0.16.2.1-cp310-abi3-win32.whl", hash = "sha256:020eebb22ed98e3bd3ab44dc9225aff0186ee72df8a152d894b7ac55b079e295", size = 395822, upload-time = "2026-09-01T12:00:04.582Z" }, { url = "https://files.pythonhosted.org/packages/57/f7/1b02fd4e531f31cdbe5fb2418b0b25587d3b721137fa9c7e457a003fd2d8/quickjs_ng-0.16.2.1-cp310-abi3-win_amd64.whl", hash = "sha256:7dd5f9fd78917c4c444c52eb2029f51610487bc219f5ca1b2766bcbd98b8afc3", size = 521986, upload-time = "2026-09-01T12:00:05.958Z" }, - { url = "https://files.pythonhosted.org/packages/9c/3b/ed008e94fd3683ee2aa68cb3675faf68b77e9d4922999888ade4cdca1b9b/quickjs_ng-0.16.2.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:7dacabcd25165e07397a19a5dc8877dc537792a6a00d47e4574922ee6ef74778", size = 475846, upload-time = "2026-09-01T12:00:07.3Z" }, - { url = "https://files.pythonhosted.org/packages/c5/50/46f4a7ea0a044ee1577e391f5a9188db5be848037305c4813487e003282e/quickjs_ng-0.16.2.1-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64e781335f02005a5df4f30276805c94a7d212d6769cd0118adce716e1012224", size = 522754, upload-time = "2026-09-01T12:00:08.724Z" }, - { url = "https://files.pythonhosted.org/packages/c2/0b/fa9bf4a1616403faf0d2cde932b814d58ed32c3e350f4566f964156d1a1f/quickjs_ng-0.16.2.1-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a089906381c0479a73f14dd46ee31c2ad26874b715e92c130bd08f51637b3b8d", size = 597645, upload-time = "2026-09-01T12:00:10.26Z" }, - { url = "https://files.pythonhosted.org/packages/62/b8/48da371ee5942d547c9c0135f2be5ddca4dac1036530eeb41611f6be128e/quickjs_ng-0.16.2.1-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d23daaec0e776f1e7c6fe0c55f396f06ff955635c4cc424e6e586158dca1436f", size = 555215, upload-time = "2026-09-01T12:00:11.602Z" }, - { url = "https://files.pythonhosted.org/packages/0e/b5/e2208efdee95fa86b1292b314a778b213aed917c3771259cd0814104efa7/quickjs_ng-0.16.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4b2818745645dec00a7659b81788654b2409a102e0e33a452353f6089b6ca807", size = 522051, upload-time = "2026-09-01T12:00:13.118Z" }, { url = "https://files.pythonhosted.org/packages/6b/1c/e5ab0fe0bd8fd668e91511b580ea2a16e04c0fd354ac9ccf58f8ad5c99d3/quickjs_ng-0.16.2.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f1c4b9741fca4cd29e0212b293a5d3a9ecda85a87d66df7bcfbcf44bef282fa2", size = 475770, upload-time = "2026-09-01T12:00:14.397Z" }, { url = "https://files.pythonhosted.org/packages/4e/4c/16a9c034989b1dbea312d0d5fd9e71af2465f5203e5140e1740bf5d43278/quickjs_ng-0.16.2.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:688942b54540df1bbca2c2c4f67a167df604df17bc76b95857b72507e195a31d", size = 522754, upload-time = "2026-09-01T12:00:15.97Z" }, { url = "https://files.pythonhosted.org/packages/4c/63/6be5cc1d20d896f1069d362668362f689c55f5b2b2c88be3bc65fe070300/quickjs_ng-0.16.2.1-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:27771a34e74aa91fc739a60c6c8c5aa4eb1d38d8ca8a1fe03485bd91d1f27a86", size = 597645, upload-time = "2026-09-01T12:00:17.427Z" }, @@ -792,60 +715,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/05/e6/2f1cdca71adb5864a97ad06c0f0b509e9ab9592b3da83f2dd5708a61ec68/termaid-0.8.0-py3-none-any.whl", hash = "sha256:3b572ed705bbca77eeffcbc1906a73f1c8119613a0552bb2ff7463a2672b1cb3", size = 159955, upload-time = "2026-07-29T08:52:04.707Z" }, ] -[[package]] -name = "tomli" -version = "2.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, - { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, - { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, - { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, - { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, - { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, - { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, - { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, - { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, - { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, - { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, - { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, - { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, - { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, - { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, - { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, - { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, - { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, - { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, - { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, - { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, - { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, - { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, - { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, - { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, - { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, - { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, - { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, - { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, - { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, - { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, - { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, - { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, - { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, - { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, - { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, - { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, - { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, - { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, - { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, - { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, - { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, - { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, - { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, -] - [[package]] name = "typing-extensions" version = "4.16.0" diff --git a/word2md/README.md b/word2md/README.md new file mode 100644 index 0000000..1bada6a --- /dev/null +++ b/word2md/README.md @@ -0,0 +1,9 @@ +# word2md + +Импорт **DOCX → Markdown** в диалекте md2gost. + +```bash +python -m word2md report.docx -o report.md +``` + +Документация: [`docs/word2md.md`](../docs/word2md.md). diff --git a/word2md/__init__.py b/word2md/__init__.py new file mode 100644 index 0000000..4b39c33 --- /dev/null +++ b/word2md/__init__.py @@ -0,0 +1,7 @@ +"""Word DOCX → Markdown (диалект md2gost).""" + +__version__ = "0.1.0" + +from .pipeline import ImportRequest, ImportResult, convert_docx + +__all__ = ["ImportRequest", "ImportResult", "convert_docx", "__version__"] diff --git a/word2md/__main__.py b/word2md/__main__.py new file mode 100644 index 0000000..ffbecf3 --- /dev/null +++ b/word2md/__main__.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python +"""CLI: DOCX → Markdown (диалект md2gost).""" + +from __future__ import annotations + +import argparse +import sys + +from .pipeline import ImportRequest, convert_docx + + +def main() -> None: + p = argparse.ArgumentParser( + prog="word2md", + description=( + "Импорт DOCX в Markdown диалекта md2gost " + "(заголовки # *, подписи %id, @Рисунок:id, таблицы, листинги)." + ), + ) + p.add_argument("filename", help="Исходный .docx") + p.add_argument( + "-o", "--output", + default=None, + help="Путь к .md (по умолчанию: .md рядом с docx)", + ) + p.add_argument( + "--media-dir", + default=None, + help="Каталог для картинок (по умолчанию: _media рядом с .md)", + ) + p.add_argument( + "--keep-toc-pages", + action=argparse.BooleanOptionalAction, + default=False, + help="Не выкидывать строки содержания Word (по умолчанию выкл.: только [TOC])", + ) + p.add_argument( + "--pagebreaks", + action=argparse.BooleanOptionalAction, + default=False, + help="Писать --- на разрывах страниц Word (для --hr-pagebreak при обратной сборке)", + ) + args = p.parse_args() + + result = convert_docx( + ImportRequest( + filename=args.filename, + output=args.output, + media_dir=args.media_dir, + keep_toc_pages=args.keep_toc_pages, + emit_page_breaks=args.pagebreaks, + ) + ) + sys.exit(result.exit_code) + + +if __name__ == "__main__": + main() diff --git a/word2md/blocks.py b/word2md/blocks.py new file mode 100644 index 0000000..16e7df7 --- /dev/null +++ b/word2md/blocks.py @@ -0,0 +1,110 @@ +"""Intermediate IR blocks between DOCX walk and Markdown emit.""" + +from __future__ import annotations + +from dataclasses import dataclass, field + + +@dataclass +class InlineSpan: + text: str + bold: bool = False + italic: bool = False + strike: bool = False + href: str | None = None # external URL + math: str | None = None # inline latex-ish + + +@dataclass +class HeadingBlock: + level: int + text: str + numbered: bool = True # False → # *TITLE + + +@dataclass +class ParagraphBlock: + spans: list[InlineSpan] = field(default_factory=list) + style: str = "Normal" + is_bibliography: bool = False + biblio_key: str | None = None + + +@dataclass +class CaptionBlock: + kind: str # figure | table | listing + number: str + text: str | None = None + is_continuation: bool = False + landscape: bool = False + unique_id: str = "" + + +@dataclass +class ImageBlock: + rel_path: str # relative to md file + alt: str = "" + caption_id: str | None = None + caption_text: str | None = None + + +@dataclass +class TableBlock: + rows: list[list[str]] + merges: list[list[tuple[str, str]]] | None = None # (v, h) per cell + caption_id: str | None = None + caption_text: str | None = None + landscape: bool = False + + +@dataclass +class ListingBlock: + lines: list[str] + language: str = "" + caption_id: str | None = None + caption_text: str | None = None + + +@dataclass +class EquationBlock: + latex: str + number: str | None = None + unique_id: str | None = None + + +@dataclass +class ListBlock: + ordered: bool + items: list[str] + level: int = 1 + + +@dataclass +class TocPlaceholder: + """Emitted as # *СОДЕРЖАНИЕ + [TOC].""" + + +@dataclass +class PageBreakBlock: + pass + + +@dataclass +class SkipNote: + """Logged skip; not emitted.""" + message: str + + +Block = ( + HeadingBlock + | ParagraphBlock + | CaptionBlock + | ImageBlock + | TableBlock + | ListingBlock + | EquationBlock + | ListBlock + | TocPlaceholder + | PageBreakBlock + | SkipNote +) diff --git a/word2md/captions.py b/word2md/captions.py new file mode 100644 index 0000000..53ace7b --- /dev/null +++ b/word2md/captions.py @@ -0,0 +1,85 @@ +"""Parse ГОСТ-style captions: Рисунок / Таблица / Листинг (+ продолжения).""" + +from __future__ import annotations + +import re +from dataclasses import dataclass + +from md2gost.word_fix import parse_caption_text + +_FIGURE_RE = re.compile( + r"^(?:Рисунок|Рис\.?)\s+(?P[\d.]+)\s*(?:[—–\-]\s*(?P.+))?$", + re.IGNORECASE, +) +_TABLE_FULL_RE = re.compile( + r"^(?:Продолжение\s+)?" + r"(?P<kind>Таблица|Таблицы|Листинг|Листинга)\s+" + r"(?P<number>[\d.]+)" + r"(?:\s*[—–\-]\s*(?P<title>.+))?$", + re.IGNORECASE, +) +_CONTINUATION_RE = re.compile(r"^Продолжение\s+(?:Таблицы|Листинга)\b", re.IGNORECASE) + + +@dataclass +class ParsedCaption: + kind: str # figure | table | listing + number: str + text: str | None = None + is_continuation: bool = False + + +def caption_unique_id(kind: str, number: str) -> str: + """Stable id from display number: 1.1 → fig1_1 / tbl1_1 / lst1_1.""" + safe = str(number).replace(".", "_") + prefix = {"figure": "fig", "table": "tbl", "listing": "lst"}.get(kind, "obj") + return f"{prefix}{safe}" + + +def parse_any_caption(text: str) -> ParsedCaption | None: + """Parse figure / table / listing captions (including «Продолжение…»).""" + raw = (text or "").replace("\r", "").replace("\x07", "").strip() + if not raw: + return None + first = raw.split("\n", 1)[0].strip() + + m = _FIGURE_RE.match(first) + if m: + title = (m.group("title") or "").strip().rstrip(".") or None + return ParsedCaption(kind="figure", number=m.group("number"), text=title) + + info = parse_caption_text(first) + if info is not None: + title = None + m2 = _TABLE_FULL_RE.match(first) + if m2 and m2.group("title"): + title = m2.group("title").strip().rstrip(".") or None + else: + # Strip leading «Таблица N — » / «Продолжение…» for leftover title + rest = _TABLE_FULL_RE.sub("", first).strip() + if rest.startswith(("—", "–", "-")): + rest = rest[1:].strip() + title = rest.rstrip(".") or None if rest else None + return ParsedCaption( + kind=info.kind, + number=info.number, + text=title, + is_continuation=info.is_continuation, + ) + + m3 = _TABLE_FULL_RE.match(first) + if m3: + kind_raw = m3.group("kind").lower() + if kind_raw.startswith("табл"): + kind = "table" + elif kind_raw.startswith("лист"): + kind = "listing" + else: + return None + return ParsedCaption( + kind=kind, + number=m3.group("number"), + text=(m3.group("title") or "").strip().rstrip(".") or None, + is_continuation=bool(_CONTINUATION_RE.match(first)), + ) + return None diff --git a/word2md/classify.py b/word2md/classify.py new file mode 100644 index 0000000..a29a8da --- /dev/null +++ b/word2md/classify.py @@ -0,0 +1,167 @@ +"""Classify DOCX paragraphs/tables into IR kinds (style first, then heuristics).""" + +from __future__ import annotations + +import re + +from md2gost.renderable.toc import SPECIAL_TITLES + +from .captions import parse_any_caption + +CAPTION_STYLE_KINDS = { + "Caption Figure": "figure", + "Название таблицы": "table", + "Caption Table": "table", + "Caption Listing": "listing", + "Caption": None, # need text heuristic +} + +HEADING_STYLE_RE = re.compile(r"^(?:Heading|Заголовок)\s*(\d+)$", re.IGNORECASE) +TOC_STYLE_RE = re.compile(r"^toc\s*\d+$", re.IGNORECASE) + +SPECIAL_UPPER = {t.upper() for t in SPECIAL_TITLES} +APPENDIX_RE = re.compile( + r"^ПРИЛОЖЕНИЕ\s+([А-ЯA-ZЁ])(?:\s+(.+))?$", + re.IGNORECASE, +) +NUMBERED_HEADING_RE = re.compile(r"^(\d+(?:\.\d+)*)\.?\s+(.+)$") +PRACTICE_RE = re.compile(r"^Практическая\s+работа\b", re.IGNORECASE) +LIST_MARKER_RE = re.compile(r"^(?:[–—\-•▪]|(\d+)\.)\s*\t?\s*(.*)$") +BIBLIO_LINE_RE = re.compile(r"^\[(\d+(?:\.\d+)?)\]\s*[:.]?\s*(.+)$") +BODY_START_TITLES = SPECIAL_UPPER | {"СОДЕРЖАНИЕ"} + + +def style_name(paragraph) -> str: + try: + if paragraph.style is None: + return "" + return paragraph.style.name or "" + except Exception: + return "" + + +def is_toc_style(name: str) -> bool: + return bool(TOC_STYLE_RE.match((name or "").strip())) + + +def heading_level_from_style(name: str) -> int | None: + m = HEADING_STYLE_RE.match((name or "").strip()) + if not m: + return None + try: + level = int(m.group(1)) + except ValueError: + return None + return level if 1 <= level <= 9 else None + + +def is_code_style(name: str) -> bool: + n = (name or "").strip().lower() + return n == "code" or n.startswith("code ") + + +def is_bibliography_style(name: str) -> bool: + n = (name or "").strip().lower() + return n in ("bibliography", "bibliography heading") + + +def is_caption_style(name: str) -> bool: + return (name or "").strip() in CAPTION_STYLE_KINDS + + +def caption_kind_from_style(name: str) -> str | None: + return CAPTION_STYLE_KINDS.get((name or "").strip()) + + +def is_special_title(text: str) -> bool: + t = (text or "").strip().upper() + if t in SPECIAL_UPPER: + return True + # allow «СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ» variants already in set + if t.startswith("СПИСОК ИСПОЛЬЗ"): + return True + return False + + +def is_body_start_heading(text: str, style: str = "") -> bool: + """True if this heading marks the start of report body (skip title pages before it).""" + t = (text or "").strip() + if not t: + return False + if heading_level_from_style(style) or style.lower().startswith("heading"): + upper = t.upper() + if upper.lstrip("* ").upper() in BODY_START_TITLES or is_special_title(t): + return True + if PRACTICE_RE.match(t): + return True + if NUMBERED_HEADING_RE.match(t): + return True + if APPENDIX_RE.match(t): + return True + # Any Heading 1 after front matter + if heading_level_from_style(style) == 1: + return True + # Heuristic without style: special titles alone + if is_special_title(t) or PRACTICE_RE.match(t): + return True + return False + + +def classify_heading_text(text: str, style_level: int | None) -> tuple[int, str, bool]: + """ + Return (level, title_text, numbered). + numbered=False → emit as # *TITLE + """ + raw = (text or "").strip() + level = style_level or 1 + + app = APPENDIX_RE.match(raw) + if app: + letter = app.group(1).upper() + name = (app.group(2) or "").strip() + title = f"Приложение {letter}" + (f" {name}" if name else "") + return 2, title, True + + if is_special_title(raw): + return 1, raw.upper(), False + + m = NUMBERED_HEADING_RE.match(raw) + if m: + # keep digits in text for manual numbering dialect + return level, raw, True + + if PRACTICE_RE.match(raw): + return 1, raw, True + + return level, raw, True + + +def look_like_list_item(text: str) -> tuple[bool, bool, str] | None: + """ + Detect md2gost-style list lines (–\\t / 1.\\t) or plain markers. + Returns (ordered, marker_ok, body) or None. + """ + t = (text or "").strip() + if not t: + return None + if t[0] in ("–", "—", "-", "•", "▪") and (len(t) == 1 or t[1] in "\t "): + body = t[1:].lstrip("\t ").strip() + return False, True, body + m = re.match(r"^(\d+)\.\s*\t?\s*(.+)$", t) + if m: + return True, True, m.group(2).strip() + return None + + +def parse_biblio_line(text: str) -> tuple[str, str] | None: + m = BIBLIO_LINE_RE.match((text or "").strip()) + if not m: + return None + return m.group(1), m.group(2).strip() + + +def paragraph_has_numpr(paragraph) -> bool: + pPr = paragraph._element.pPr + if pPr is None: + return False + return pPr.numPr is not None diff --git a/word2md/emit.py b/word2md/emit.py new file mode 100644 index 0000000..f446699 --- /dev/null +++ b/word2md/emit.py @@ -0,0 +1,249 @@ +"""Emit md2gost dialect Markdown from IR blocks.""" + +from __future__ import annotations + +from .blocks import ( + Block, + CaptionBlock, + EquationBlock, + HeadingBlock, + ImageBlock, + InlineSpan, + ListBlock, + ListingBlock, + PageBreakBlock, + ParagraphBlock, + TableBlock, + TocPlaceholder, +) + +# Characters that must stay literal after DOCX→MD (Word stores bare '*', not '\*'). +# Do not escape '_': md2gost labels use it (@Рисунок:fig1_1). +_MD_SPECIAL = str.maketrans( + { + "\\": "\\\\", + "*": "\\*", + "`": "\\`", + } +) + + +def escape_md_inline(text: str) -> str: + """Escape Markdown inline metacharacters in plain text from DOCX.""" + if not text: + return "" + return text.translate(_MD_SPECIAL) + + +def _escape_md_cell(text: str) -> str: + return escape_md_inline(text or "").replace("|", "\\|").replace("\n", " ") + + +def _render_spans(spans: list[InlineSpan]) -> str: + parts: list[str] = [] + for s in spans: + if s.math: + parts.append(f"${s.math}$") + continue + text = escape_md_inline(s.text or "") + if s.href: + parts.append(f"[{text}]({s.href})") + continue + if s.bold and s.italic: + text = f"***{text}***" + elif s.bold: + text = f"**{text}**" + elif s.italic: + text = f"*{text}*" + if s.strike: + text = f"~~{text}~~" + parts.append(text) + return "".join(parts) + + +def _emit_caption_line(unique_id: str, text: str | None = None, *, landscape: bool = False) -> str: + title = escape_md_inline((text or "").strip()) + flags = " +landscape" if landscape else "" + if title: + return f"%{unique_id} {title}{flags}".rstrip() + return f"%{unique_id}{flags}".rstrip() + + +def _last_nonempty(chunks: list[str]) -> str: + for prev in reversed(chunks): + if prev.strip(): + return prev + return "" + + +def _emit_table(tbl: TableBlock) -> str: + if not tbl.rows: + return "" + lines: list[str] = [] + for r, row in enumerate(tbl.rows): + cells = [] + for c, cell in enumerate(row): + if tbl.merges and r < len(tbl.merges) and c < len(tbl.merges[r]): + v, h = tbl.merges[r][c] + if h == "continue": + cells.append(">") + continue + if v == "continue": + cells.append("^") + continue + cells.append(_escape_md_cell(cell if cell not in ("^", ">") else cell)) + lines.append("| " + " | ".join(cells) + " |") + if r == 0: + lines.append("| " + " | ".join("---" for _ in cells) + " |") + return "\n".join(lines) + + +def emit_markdown(blocks: list[Block], *, emit_page_breaks: bool = False) -> str: + chunks: list[str] = [] + i = 0 + while i < len(blocks): + b = blocks[i] + + if isinstance(b, HeadingBlock): + hashes = "#" * max(1, min(b.level, 6)) + title = escape_md_inline(b.text or "") + if not b.numbered: + # Dialect flag '*' after '# ' is structural — not escaped. + chunks.append(f"{hashes} *{title}") + else: + chunks.append(f"{hashes} {title}") + chunks.append("") + i += 1 + continue + + if isinstance(b, TocPlaceholder): + chunks.append("[TOC]") + chunks.append("") + i += 1 + continue + + if isinstance(b, CaptionBlock): + if b.is_continuation: + i += 1 + continue + nxt = blocks[i + 1] if i + 1 < len(blocks) else None + if isinstance(nxt, ImageBlock) and b.kind == "figure": + nxt.caption_id = nxt.caption_id or b.unique_id + nxt.caption_text = nxt.caption_text or b.text + i += 1 + continue + if isinstance(nxt, TableBlock) and b.kind == "table": + nxt.caption_id = nxt.caption_id or b.unique_id + nxt.caption_text = nxt.caption_text or b.text + if b.landscape: + nxt.landscape = True + chunks.append(_emit_caption_line( + nxt.caption_id or b.unique_id, + nxt.caption_text or b.text, + landscape=nxt.landscape, + )) + chunks.append("") + i += 1 + continue + if isinstance(nxt, ListingBlock) and b.kind == "listing": + nxt.caption_id = nxt.caption_id or b.unique_id + nxt.caption_text = nxt.caption_text or b.text + chunks.append(_emit_caption_line(nxt.caption_id or b.unique_id, nxt.caption_text)) + chunks.append("") + i += 1 + continue + chunks.append(_emit_caption_line(b.unique_id, b.text, landscape=b.landscape)) + chunks.append("") + i += 1 + continue + + if isinstance(b, ImageBlock): + # Safety: caption still after image in IR + nxt = blocks[i + 1] if i + 1 < len(blocks) else None + if ( + isinstance(nxt, CaptionBlock) + and nxt.kind == "figure" + and not nxt.is_continuation + ): + b.caption_id = b.caption_id or nxt.unique_id + b.caption_text = b.caption_text or nxt.text + # skip the caption block + i += 1 + title = "" + if b.caption_id: + title_body = escape_md_inline((b.caption_text or "").strip()) + title = f" \"%{b.caption_id}" + (f" {title_body}" if title_body else "") + "\"" + alt = escape_md_inline((b.alt or "рисунок").replace('"', "'")) + chunks.append(f"![{alt}]({b.rel_path}{title})") + chunks.append("") + i += 1 + continue + + if isinstance(b, TableBlock): + if b.caption_id: + prev = _last_nonempty(chunks) + if not prev.startswith(f"%{b.caption_id}"): + chunks.append(_emit_caption_line(b.caption_id, b.caption_text, landscape=b.landscape)) + chunks.append("") + chunks.append(_emit_table(b)) + chunks.append("") + i += 1 + continue + + if isinstance(b, ListingBlock): + if b.caption_id: + prev = _last_nonempty(chunks) + if not prev.startswith(f"%{b.caption_id}"): + chunks.append(_emit_caption_line(b.caption_id, b.caption_text)) + chunks.append("") + lang = b.language or "" + body = "\n".join(line.rstrip("\n") for line in b.lines) + chunks.append(f"```{lang}") + chunks.append(body) + chunks.append("```") + chunks.append("") + i += 1 + continue + + if isinstance(b, EquationBlock): + if b.unique_id: + chunks.append(f"%{b.unique_id}") + chunks.append("$$") + chunks.append(b.latex.strip()) + chunks.append("$$") + chunks.append("") + i += 1 + continue + + if isinstance(b, ListBlock): + for idx, item in enumerate(b.items, start=1): + body = escape_md_inline(item) + if b.ordered: + chunks.append(f"{idx}. {body}") + else: + chunks.append(f"- {body}") + chunks.append("") + i += 1 + continue + + if isinstance(b, ParagraphBlock): + line = _render_spans(b.spans).strip() + if line: + chunks.append(line) + chunks.append("") + i += 1 + continue + + if isinstance(b, PageBreakBlock): + if emit_page_breaks: + chunks.append("---") + chunks.append("") + i += 1 + continue + + i += 1 + + text = "\n".join(chunks) + while "\n\n\n" in text: + text = text.replace("\n\n\n", "\n\n") + return text.strip() + "\n" diff --git a/word2md/media.py b/word2md/media.py new file mode 100644 index 0000000..46ceac6 --- /dev/null +++ b/word2md/media.py @@ -0,0 +1,91 @@ +"""Extract images from a DOCX package into a media folder.""" + +from __future__ import annotations + +import hashlib +import os +import zipfile +from pathlib import Path + +from docx.oxml.ns import qn + + +def _r_id_from_blip(blip) -> str | None: + if blip is None: + return None + return blip.get(qn("r:embed")) or blip.get( + "{http://schemas.openxmlformats.org/officeDocument/2006/relationships}embed" + ) + + +def iter_blip_rids(paragraph_element) -> list[str]: + """Collect relationship ids for drawings in a paragraph.""" + if paragraph_element is None: + return [] + rids: list[str] = [] + for blip in paragraph_element.findall(".//" + qn("a:blip")): + rid = _r_id_from_blip(blip) + if rid: + rids.append(rid) + return rids + + +class MediaExtractor: + """Copy word/media parts referenced by rIds into dest_dir.""" + + def __init__(self, docx_path: str | Path, dest_dir: str | Path, *, rel_prefix: str): + self.docx_path = Path(docx_path) + self.dest_dir = Path(dest_dir) + self.rel_prefix = rel_prefix.replace("\\", "/").rstrip("/") + self.dest_dir.mkdir(parents=True, exist_ok=True) + self._zf = zipfile.ZipFile(self.docx_path, "r") + self._cache: dict[str, str] = {} # rid -> relative path for md + self._used_names: set[str] = set() + + def close(self) -> None: + self._zf.close() + + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() + + def extract_rid(self, document_part, rid: str) -> str | None: + """Return path relative to the .md file, or None on failure.""" + if rid in self._cache: + return self._cache[rid] + try: + rel = document_part.rels[rid] + except KeyError: + return None + target = getattr(rel, "target_ref", None) or getattr(rel, "target_part", None) + if target is None: + return None + # target_ref like "media/image1.png" + if hasattr(rel, "target_part"): + part = rel.target_part + blob = part.blob + name = os.path.basename(part.partname) + else: + ref = str(target).lstrip("/") + zip_name = "word/" + ref if not ref.startswith("word/") else ref + try: + blob = self._zf.read(zip_name) + except KeyError: + return None + name = os.path.basename(ref) + + stem, ext = os.path.splitext(name) + if not ext: + ext = ".png" + safe = f"{stem}{ext}" + if safe in self._used_names: + digest = hashlib.md5(blob).hexdigest()[:8] + safe = f"{stem}_{digest}{ext}" + self._used_names.add(safe) + out_path = self.dest_dir / safe + out_path.write_bytes(blob) + rel_path = f"{self.rel_prefix}/{safe}" if self.rel_prefix else safe + self._cache[rid] = rel_path + return rel_path diff --git a/word2md/omml.py b/word2md/omml.py new file mode 100644 index 0000000..bcd0ed0 --- /dev/null +++ b/word2md/omml.py @@ -0,0 +1,149 @@ +"""Best-effort OMML → readable LaTeX-like string for $$ / $ fences.""" + +from __future__ import annotations + +_OMML_NS = "http://schemas.openxmlformats.org/officeDocument/2006/math" + + +def _local(tag: str) -> str: + if "}" in tag: + return tag.rsplit("}", 1)[-1] + return tag + + +def omml_to_latex(element) -> str: + """Flatten OMML tree to a readable formula string (not perfect LaTeX).""" + if element is None: + return "" + parts: list[str] = [] + _walk(element, parts) + text = "".join(parts) + text = " ".join(text.split()) + return text.strip() + + +def _walk(node, parts: list[str]) -> None: + tag = _local(node.tag) + + if tag == "t": + parts.append(node.text or "") + return + + if tag == "f": # fraction + num = _child(node, "num") + den = _child(node, "den") + parts.append(r"\frac{") + if num is not None: + _walk(num, parts) + parts.append("}{") + if den is not None: + _walk(den, parts) + parts.append("}") + return + + if tag == "sSup": + base = _child(node, "e") + sup = _child(node, "sup") + if base is not None: + _walk(base, parts) + parts.append("^{") + if sup is not None: + _walk(sup, parts) + parts.append("}") + return + + if tag == "sSub": + base = _child(node, "e") + sub = _child(node, "sub") + if base is not None: + _walk(base, parts) + parts.append("_{") + if sub is not None: + _walk(sub, parts) + parts.append("}") + return + + if tag == "sSubSup": + base = _child(node, "e") + sub = _child(node, "sub") + sup = _child(node, "sup") + if base is not None: + _walk(base, parts) + parts.append("_{") + if sub is not None: + _walk(sub, parts) + parts.append("}^{") + if sup is not None: + _walk(sup, parts) + parts.append("}") + return + + if tag == "rad": + deg = _child(node, "deg") + e = _child(node, "e") + if deg is not None and "".join(deg.itertext()).strip(): + parts.append(r"\sqrt[") + _walk(deg, parts) + parts.append("]{") + else: + parts.append(r"\sqrt{") + if e is not None: + _walk(e, parts) + parts.append("}") + return + + if tag == "nary": + for child in node: + _walk(child, parts) + return + + if tag in ("oMath", "oMathPara", "e", "num", "den", "sup", "sub", "deg", + "fPr", "ctrlPr", "rPr", "sSupPr", "sSubPr", "sSubSupPr", "radPr", + "naryPr", "dPr", "boxPr", "argSz"): + for child in node: + _walk(child, parts) + return + + if tag == "r": + for child in node: + _walk(child, parts) + return + + if tag == "d": # delimiter + parts.append("(") + for child in node: + if _local(child.tag) != "dPr": + _walk(child, parts) + parts.append(")") + return + + if tag in ("pPr", "rFonts", "sz", "szCs", "color", "jc"): + return + + for child in node: + _walk(child, parts) + if node.tail: + parts.append(node.tail) + + +def _child(node, local_name: str): + for child in node: + if _local(child.tag) == local_name: + return child + return None + + +def find_omml(paragraph_element) -> list: + """Return all m:oMath / m:oMathPara elements under a paragraph.""" + if paragraph_element is None: + return [] + found = [] + for el in paragraph_element.iter(): + tag = _local(el.tag) + if tag in ("oMath", "oMathPara"): + found.append(el) + return found + + +def paragraph_has_omml(paragraph_element) -> bool: + return bool(find_omml(paragraph_element)) diff --git a/word2md/pipeline.py b/word2md/pipeline.py new file mode 100644 index 0000000..ef83601 --- /dev/null +++ b/word2md/pipeline.py @@ -0,0 +1,110 @@ +"""DOCX → md2gost Markdown pipeline.""" + +from __future__ import annotations + +import traceback +from dataclasses import dataclass +from pathlib import Path +from typing import Callable + +from docx import Document + +from .emit import emit_markdown +from .media import MediaExtractor +from .refs import apply_reference_pass +from .walker import DocxWalker, postprocess_blocks + +LogFn = Callable[[str], None] + + +@dataclass +class ImportRequest: + filename: str = "" + output: str | None = None + media_dir: str | None = None + keep_toc_pages: bool = False + emit_page_breaks: bool = False + + +@dataclass +class ImportResult: + ok: bool + exit_code: int = 0 + output_path: str | None = None + media_dir: str | None = None + message: str = "" + + +def default_output_path(docx_path: str | Path) -> Path: + p = Path(docx_path) + return p.with_suffix(".md") + + +def default_media_dir(md_path: Path) -> Path: + return md_path.parent / f"{md_path.stem}_media" + + +def convert_docx(req: ImportRequest, log: LogFn | None = None) -> ImportResult: + """Convert DOCX to md2gost dialect Markdown. Errors go into ImportResult.""" + emit: LogFn = log if log is not None else print + + def fail(code: int, message: str) -> ImportResult: + emit(message) + return ImportResult(False, code, message=message) + + filename = (req.filename or "").strip() + if not filename: + return fail(2, "Укажите исходный .docx") + path = Path(filename) + if not path.is_file(): + return fail(2, f"Файл не найден: {path}") + if path.suffix.lower() != ".docx": + return fail(1, "Исходный файл должен быть в формате .docx") + + out = Path(req.output) if req.output else default_output_path(path) + if out.suffix.lower() != ".md": + out = out.with_suffix(".md") + out.parent.mkdir(parents=True, exist_ok=True) + + media_path = Path(req.media_dir) if req.media_dir else default_media_dir(out) + rel_prefix = media_path.name + # If media dir is not sibling of md, use relative path from md parent + try: + rel_prefix = str(media_path.resolve().relative_to(out.parent.resolve())).replace("\\", "/") + except ValueError: + rel_prefix = str(media_path).replace("\\", "/") + + try: + document = Document(str(path)) + except Exception as exc: + return fail(1, f"Не удалось открыть DOCX: {exc}") + + try: + with MediaExtractor(path, media_path, rel_prefix=rel_prefix) as media: + walker = DocxWalker( + document, + media, + keep_toc_pages=req.keep_toc_pages, + emit_page_breaks=req.emit_page_breaks, + log=emit, + ) + blocks = walker.walk() + blocks = postprocess_blocks(blocks) + blocks = apply_reference_pass(blocks) + md_text = emit_markdown(blocks, emit_page_breaks=req.emit_page_breaks) + out.write_text(md_text, encoding="utf-8") + except Exception as exc: + emit(traceback.format_exc()) + return fail(1, f"Ошибка импорта: {exc}") + + msg = f"Markdown: {out}" + emit(msg) + if media_path.is_dir() and any(media_path.iterdir()): + emit(f"Медиа: {media_path}") + return ImportResult( + True, + 0, + output_path=str(out.resolve()), + media_dir=str(media_path.resolve()) if media_path.exists() else None, + message=msg, + ) diff --git a/word2md/refs.py b/word2md/refs.py new file mode 100644 index 0000000..e3fea0b --- /dev/null +++ b/word2md/refs.py @@ -0,0 +1,170 @@ +"""Second pass: replace «Рисунок 1.1» / «табл. 2» with @Тип:id.""" + +from __future__ import annotations + +import re + +from .blocks import ( + Block, + CaptionBlock, + EquationBlock, + ImageBlock, + InlineSpan, + ListBlock, + ListingBlock, + ParagraphBlock, + TableBlock, +) +from .captions import caption_unique_id + +# Number like 1 / 1.1 / 2.3.4 — do NOT use [\d.]+ (eats trailing sentence period). +_NUM = r"(?P<num>\d+(?:\.\d+)*)" + +_REF_PATTERNS = [ + ( + re.compile( + rf"(?P<full>(?:Рисунк(?:е|а|у|ом)?|Рисунок|Рис\.?)\s+{_NUM})", + re.IGNORECASE, + ), + "figure", + ), + ( + re.compile( + rf"(?P<full>(?:Таблиц[аыеу]|табл\.?)\s+{_NUM})", + re.IGNORECASE, + ), + "table", + ), + ( + re.compile( + rf"(?P<full>(?:Листинг(?:а|у|е|ом)?|лист\.?)\s+{_NUM})", + re.IGNORECASE, + ), + "listing", + ), + ( + re.compile( + rf"(?P<full>(?:Формул[аыеу]|форм\.?)\s+{_NUM})", + re.IGNORECASE, + ), + "equation", + ), +] + +_TYPE_LABEL = { + "figure": "Рисунок", + "table": "Таблица", + "listing": "Листинг", + "equation": "Формула", +} + + +def collect_number_maps(blocks: list[Block]) -> dict[str, dict[str, str]]: + """kind → {number → unique_id}.""" + maps: dict[str, dict[str, str]] = { + "figure": {}, + "table": {}, + "listing": {}, + "equation": {}, + } + for b in blocks: + if isinstance(b, CaptionBlock) and not b.is_continuation: + maps[b.kind][b.number] = b.unique_id or caption_unique_id(b.kind, b.number) + elif isinstance(b, ImageBlock) and b.caption_id: + num = _id_to_number(b.caption_id, "fig") + if num: + maps["figure"][num] = b.caption_id + elif isinstance(b, TableBlock) and b.caption_id: + num = _id_to_number(b.caption_id, "tbl") + if num: + maps["table"][num] = b.caption_id + elif isinstance(b, ListingBlock) and b.caption_id: + num = _id_to_number(b.caption_id, "lst") + if num: + maps["listing"][num] = b.caption_id + elif isinstance(b, EquationBlock) and b.number and b.unique_id: + maps["equation"][b.number] = b.unique_id + return maps + + +def _id_to_number(uid: str, prefix: str) -> str | None: + if not uid.startswith(prefix): + return None + rest = uid[len(prefix):] + if not rest: + return None + return rest.replace("_", ".") + + +def rewrite_text(text: str, maps: dict[str, dict[str, str]]) -> str: + """Replace object references; leave bibliography [1] alone.""" + if not text: + return text + + def repl_factory(kind: str): + label = _TYPE_LABEL[kind] + num_map = maps.get(kind) or {} + + def repl(m: re.Match) -> str: + num = m.group("num") + uid = num_map.get(num) + if not uid: + return m.group(0) + return f"@{label}:{uid}" + + return repl + + out = text + for pattern, kind in _REF_PATTERNS: + out = pattern.sub(repl_factory(kind), out) + return out + + +def _rewrite_paragraph_spans(para: ParagraphBlock, maps: dict[str, dict[str, str]]) -> None: + """Rewrite refs even when Word split «Таблице 5.1» across runs.""" + spans = para.spans + if not spans: + return + new_spans: list[InlineSpan] = [] + i = 0 + while i < len(spans): + s = spans[i] + if s.math or s.href: + new_spans.append(s) + i += 1 + continue + j = i + chunk: list[InlineSpan] = [] + while j < len(spans) and not spans[j].math and not spans[j].href: + chunk.append(spans[j]) + j += 1 + joined = "".join(p.text or "" for p in chunk) + rewritten = rewrite_text(joined, maps) + if rewritten == joined: + new_spans.extend(chunk) + else: + first = chunk[0] + same_fmt = all( + p.bold == first.bold and p.italic == first.italic and p.strike == first.strike + for p in chunk + ) + new_spans.append( + InlineSpan( + text=rewritten, + bold=first.bold if same_fmt else False, + italic=first.italic if same_fmt else False, + strike=first.strike if same_fmt else False, + ) + ) + i = j + para.spans = new_spans + + +def apply_reference_pass(blocks: list[Block]) -> list[Block]: + maps = collect_number_maps(blocks) + for b in blocks: + if isinstance(b, ParagraphBlock): + _rewrite_paragraph_spans(b, maps) + elif isinstance(b, ListBlock): + b.items = [rewrite_text(item, maps) for item in b.items] + return blocks diff --git a/word2md/tables.py b/word2md/tables.py new file mode 100644 index 0000000..1101bca --- /dev/null +++ b/word2md/tables.py @@ -0,0 +1,140 @@ +"""DOCX table → markdown rows with ^ / > merge markers.""" + +from __future__ import annotations + +from docx.oxml.ns import qn +from docx.table import Table as DocxTable + + +def _cell_v_merge(tc) -> str: + """Return none | restart | continue for vertical merge.""" + tcPr = tc.tcPr + if tcPr is None: + return "none" + vMerge = tcPr.find(qn("w:vMerge")) + if vMerge is None: + return "none" + val = vMerge.get(qn("w:val")) + if val in (None, "continue"): + return "continue" + return "restart" + + +def _cell_grid_span(tc) -> int: + tcPr = tc.tcPr + if tcPr is None: + return 1 + span = tcPr.find(qn("w:gridSpan")) + if span is None: + return 1 + try: + return max(1, int(span.get(qn("w:val")) or "1")) + except ValueError: + return 1 + + +def _cell_text(cell) -> str: + from .text import paragraph_plain + + parts: list[str] = [] + for p in cell.paragraphs: + t = paragraph_plain(p) + if t: + parts.append(t) + return " ".join(parts).replace("|", "\\|") + + +def table_to_grid(docx_table: DocxTable) -> tuple[list[list[str]], list[list[tuple[str, str]]]]: + """ + Expand a Word table into a rectangular grid. + + merges[r][c] = (v, h) where v/h in {none, restart, continue}. + Horizontally spanned continue cells are filled with '>' semantics. + Vertically continued cells use '^'. + """ + rows_xml = docx_table._tbl.tr_lst + # First pass: determine max columns from grid spans + max_cols = 0 + row_infos: list[list[tuple[object, int, str]]] = [] # (tc, span, vmerge) + for tr in rows_xml: + info: list[tuple[object, int, str]] = [] + col = 0 + for tc in tr.tc_lst: + span = _cell_grid_span(tc) + v = _cell_v_merge(tc) + info.append((tc, span, v)) + col += span + max_cols = max(max_cols, col) + row_infos.append(info) + + if max_cols == 0: + return [], [] + + texts: list[list[str]] = [["-" for _ in range(max_cols)] for _ in range(len(row_infos))] + merges: list[list[tuple[str, str]]] = [ + [("none", "none") for _ in range(max_cols)] for _ in range(len(row_infos)) + ] + + # Map tc elements to python-docx cells by index + for r, info in enumerate(row_infos): + c = 0 + cell_idx = 0 + for tc, span, v in info: + try: + cell = docx_table.rows[r].cells[cell_idx] + text = _cell_text(cell) + except Exception: + text = "" + cell_idx += 1 + + if v == "continue": + merges[r][c] = ("continue", "none") + texts[r][c] = "^" + else: + merges[r][c] = ("restart" if v == "restart" else "none", "none" if span == 1 else "restart") + texts[r][c] = text if text else " " + for k in range(1, span): + if c + k < max_cols: + merges[r][c + k] = ("none", "continue") + texts[r][c + k] = ">" + c += span + + return texts, merges + + +def is_formula_table(docx_table: DocxTable) -> bool: + """Heuristic: 1×2 table with Formula Content / Formula Numbering styles.""" + try: + if len(docx_table.rows) != 1 or len(docx_table.columns) != 2: + return False + except Exception: + return False + styles: list[str] = [] + for cell in docx_table.rows[0].cells: + for p in cell.paragraphs: + name = (p.style.name if p.style is not None else "") or "" + styles.append(name) + joined = " ".join(styles).lower() + return "formula" in joined + + +def formula_table_parts(docx_table: DocxTable) -> tuple[object | None, str | None]: + """Return (omml_element_or_None, number_text_or_None) from a formula table.""" + from .omml import find_omml, omml_to_latex + from .text import paragraph_plain + + left = docx_table.rows[0].cells[0] + right = docx_table.rows[0].cells[1] + latex = "" + for p in left.paragraphs: + for om in find_omml(p._element): + latex = omml_to_latex(om) + if latex: + break + if latex: + break + if not latex: + latex = " ".join(paragraph_plain(p) for p in left.paragraphs).strip() + number = " ".join(paragraph_plain(p) for p in right.paragraphs).strip() + number = number.strip("() ").strip() or None + return latex, number diff --git a/word2md/text.py b/word2md/text.py new file mode 100644 index 0000000..bd134fc --- /dev/null +++ b/word2md/text.py @@ -0,0 +1,78 @@ +"""Extract visible text from OOXML runs/paragraphs (incl. w:noBreakHyphen).""" + +from __future__ import annotations + +from docx.oxml.ns import qn + + +def _local(tag: str) -> str: + if "}" in tag: + return tag.rsplit("}", 1)[-1] + return tag + + +def run_element_text(run_el) -> str: + """ + Text of a w:r including non-breaking hyphens. + + md2gost writes ASCII '-' as w:noBreakHyphen (python-docx run.text skips it). + Soft hyphens are optional break hints — omit them from Markdown. + """ + if run_el is None: + return "" + parts: list[str] = [] + for child in run_el: + tag = _local(child.tag) + if tag == "t": + parts.append(child.text or "") + elif tag == "noBreakHyphen": + parts.append("-") + elif tag == "softHyphen": + continue + elif tag == "tab": + parts.append("\t") + elif tag == "br": + # page/line break inside run — treat as space for inline text + if child.get(qn("w:type")) == "page": + continue + parts.append("\n") + elif tag == "cr": + parts.append("\n") + return "".join(parts) + + +def hyperlink_element_text(hyperlink_el) -> str: + if hyperlink_el is None: + return "" + parts: list[str] = [] + for run in hyperlink_el.findall(qn("w:r")): + parts.append(run_element_text(run)) + return "".join(parts) + + +def paragraph_element_text(p_el) -> str: + """Visible body text of w:p (runs + hyperlinks), without OMML.""" + if p_el is None: + return "" + parts: list[str] = [] + for child in p_el: + tag = child.tag + if tag == qn("w:r"): + parts.append(run_element_text(child)) + elif tag == qn("w:hyperlink"): + parts.append(hyperlink_element_text(child)) + return "".join(parts) + + +def paragraph_plain(paragraph) -> str: + """Strip CR/bell from a python-docx Paragraph, keeping noBreakHyphen as '-'.""" + return paragraph_raw(paragraph).strip() + + +def paragraph_raw(paragraph) -> str: + """Full paragraph text with noBreakHyphen → '-', without strip (for Code lines).""" + try: + text = paragraph_element_text(paragraph._element) + except Exception: + text = paragraph.text or "" + return text.replace("\r", "").replace("\x07", "") diff --git a/word2md/walker.py b/word2md/walker.py new file mode 100644 index 0000000..048a857 --- /dev/null +++ b/word2md/walker.py @@ -0,0 +1,596 @@ +"""Walk DOCX body and produce IR blocks.""" + +from __future__ import annotations + +import re +from typing import Callable + +from docx import Document +from docx.oxml.ns import qn +from docx.table import Table as DocxTable +from docx.text.paragraph import Paragraph as DocxParagraph + +from .blocks import ( + Block, + CaptionBlock, + EquationBlock, + HeadingBlock, + ImageBlock, + InlineSpan, + ListBlock, + ListingBlock, + PageBreakBlock, + ParagraphBlock, + SkipNote, + TableBlock, + TocPlaceholder, +) +from .captions import caption_unique_id, parse_any_caption +from .classify import ( + caption_kind_from_style, + classify_heading_text, + heading_level_from_style, + is_bibliography_style, + is_body_start_heading, + is_caption_style, + is_code_style, + is_special_title, + is_toc_style, + look_like_list_item, + parse_biblio_line, + paragraph_has_numpr, + style_name, +) +from .media import MediaExtractor, iter_blip_rids +from .omml import find_omml, omml_to_latex, paragraph_has_omml +from .tables import formula_table_parts, is_formula_table, table_to_grid +from .text import ( + hyperlink_element_text, + paragraph_plain, + paragraph_raw, + run_element_text, +) + +LogFn = Callable[[str], None] + + +def _para_plain(paragraph: DocxParagraph) -> str: + return paragraph_plain(paragraph) + + +def _iter_body_items(document: Document): + body = document.element.body + for child in body.iterchildren(): + tag = child.tag + if tag == qn("w:p"): + yield "p", DocxParagraph(child, document) + elif tag == qn("w:tbl"): + yield "tbl", DocxTable(child, document) + elif tag == qn("w:sectPr"): + continue + else: + yield "other", child + + +def _inline_spans(paragraph: DocxParagraph) -> list[InlineSpan]: + """Build inline spans from runs + hyperlinks + inline math.""" + spans: list[InlineSpan] = [] + p = paragraph._element + for child in p: + tag = child.tag + if tag == qn("w:r"): + text = run_element_text(child) + if child.find(qn("w:br")) is not None and not text.strip(): + # lone page/line break run + continue + if not text: + continue + rPr = child.find(qn("w:rPr")) + bold = italic = strike = False + if rPr is not None: + bold = rPr.find(qn("w:b")) is not None + italic = rPr.find(qn("w:i")) is not None + strike = rPr.find(qn("w:strike")) is not None or rPr.find(qn("w:dstrike")) is not None + spans.append(InlineSpan(text=text, bold=bold, italic=italic, strike=strike)) + elif tag == qn("w:hyperlink"): + anchor = child.get(qn("w:anchor")) + rid = child.get(qn("r:id")) + href = None + if rid: + try: + rel = paragraph.part.rels[rid] + href = rel.target_ref + except Exception: + href = None + inner = hyperlink_element_text(child) + if not inner: + continue + # Internal biblio links → keep plain [n] text (no md link) + if anchor and str(anchor).startswith("biblio_"): + spans.append(InlineSpan(text=inner)) + elif href and not str(href).startswith("#"): + spans.append(InlineSpan(text=inner, href=href)) + else: + spans.append(InlineSpan(text=inner)) + elif tag.endswith("}oMath") or tag.endswith("}oMathPara"): + latex = omml_to_latex(child) + if latex: + spans.append(InlineSpan(text="", math=latex)) + # Fallback if empty but paragraph has text + if not spans: + plain = _para_plain(paragraph) + if plain: + spans.append(InlineSpan(text=plain)) + return spans + + +def _section_landscape(document: Document, element) -> bool: + """Best-effort: check nearest following sectPr or document default.""" + try: + # Walk forward siblings for sectPr + parent = element.getparent() + if parent is None: + return False + el = element.getnext() + while el is not None: + if el.tag == qn("w:sectPr"): + pgSz = el.find(qn("w:pgSz")) + if pgSz is not None: + w = int(pgSz.get(qn("w:w") or "0") or 0) + h = int(pgSz.get(qn("w:h") or "0") or 0) + orient = pgSz.get(qn("w:orient")) + if orient == "landscape" or (w and h and w > h): + return True + return False + el = el.getnext() + # last section + sect = document.sections[-1] + return bool(sect.page_width and sect.page_height and sect.page_width > sect.page_height) + except Exception: + return False + + +class DocxWalker: + def __init__( + self, + document: Document, + media: MediaExtractor, + *, + keep_toc_pages: bool = False, + emit_page_breaks: bool = False, + log: LogFn | None = None, + ): + self.document = document + self.media = media + self.keep_toc_pages = keep_toc_pages + self.emit_page_breaks = emit_page_breaks + self.log = log or (lambda _m: None) + self._started = False + self._saw_toc_heading = False + self._pending_caption: CaptionBlock | None = None + self._code_buf: list[str] = [] + self._code_caption: CaptionBlock | None = None + self._list_buf: list[str] | None = None + self._list_ordered: bool | None = None + + def walk(self) -> list[Block]: + blocks: list[Block] = [] + for kind, item in _iter_body_items(self.document): + if kind == "p": + blocks.extend(self._handle_paragraph(item)) + elif kind == "tbl": + self._flush_code(blocks) + self._flush_list(blocks) + blocks.extend(self._handle_table(item)) + else: + self.log(f"Пропуск неизвестного элемента body: {getattr(item, 'tag', item)}") + blocks.append(SkipNote(message=f"unknown body child {getattr(item, 'tag', '')}")) + self._flush_code(blocks) + self._flush_list(blocks) + return [b for b in blocks if not isinstance(b, SkipNote)] + + def _flush_code(self, blocks: list[Block]) -> None: + if not self._code_buf: + self._code_caption = None + return + cap = self._code_caption + uid = cap.unique_id if cap else None + text = cap.text if cap else None + blocks.append( + ListingBlock( + lines=list(self._code_buf), + language="", + caption_id=uid, + caption_text=text, + ) + ) + self._code_buf.clear() + self._code_caption = None + self._pending_caption = None + + def _flush_list(self, blocks: list[Block]) -> None: + if self._list_buf is None: + return + blocks.append( + ListBlock( + ordered=bool(self._list_ordered), + items=list(self._list_buf), + ) + ) + self._list_buf = None + self._list_ordered = None + + def _handle_paragraph(self, paragraph: DocxParagraph) -> list[Block]: + out: list[Block] = [] + style = style_name(paragraph) + text = _para_plain(paragraph) + el = paragraph._element + + # Page break + if self.emit_page_breaks: + for br in el.findall(".//" + qn("w:br")): + if br.get(qn("w:type")) == "page": + self._flush_code(out) + self._flush_list(out) + out.append(PageBreakBlock()) + + # Skip until body start (title / assignment) + if not self._started: + if is_body_start_heading(text, style) or heading_level_from_style(style): + if is_body_start_heading(text, style) or heading_level_from_style(style) == 1: + self._started = True + elif not text: + return out + else: + return out + else: + return out + + # TOC field lines + if is_toc_style(style): + if not self.keep_toc_pages: + return out + # keep as plain paragraphs if requested + self._flush_code(out) + self._flush_list(out) + out.append(ParagraphBlock(spans=_inline_spans(paragraph), style=style)) + return out + + # Native TOC field detection + if self._has_toc_field(el): + self._flush_code(out) + self._flush_list(out) + if not self.keep_toc_pages: + # only placeholder once after СОДЕРЖАНИЕ heading + return out + return out + + level = heading_level_from_style(style) + if level is not None or (text and is_special_title(text) and not text.startswith("[")): + self._flush_code(out) + self._flush_list(out) + if not text: + return out + lvl, title, numbered = classify_heading_text(text, level or 1) + if title.upper() == "СОДЕРЖАНИЕ" or title.upper().startswith("СОДЕРЖАНИЕ"): + out.append(HeadingBlock(level=1, text="СОДЕРЖАНИЕ", numbered=False)) + out.append(TocPlaceholder()) + self._saw_toc_heading = True + self._pending_caption = None + return out + out.append(HeadingBlock(level=lvl, text=title, numbered=numbered)) + self._pending_caption = None + return out + + # Caption styles / heuristic captions + if is_caption_style(style) or parse_any_caption(text): + parsed = parse_any_caption(text) + kind = caption_kind_from_style(style) if is_caption_style(style) else None + if parsed is None and kind: + # style known but text odd — treat whole text as title + parsed = parse_any_caption(text) # may still fail + if parsed is not None: + self._flush_code(out) + self._flush_list(out) + if kind and parsed.kind != kind and kind != "figure": + # prefer text kind when style is generic Caption + pass + uid = caption_unique_id(parsed.kind, parsed.number) + cap = CaptionBlock( + kind=parsed.kind, + number=parsed.number, + text=parsed.text, + is_continuation=parsed.is_continuation, + unique_id=uid, + ) + if parsed.is_continuation: + # Emit marker for postprocess_blocks to splice into previous table/listing. + # Do NOT stash in _pending_caption — that created a second %tbl / table. + self._pending_caption = None + out.append(cap) + return out + # Tables/listings: caption precedes object → keep pending. + # Figures (GOST): caption is under the picture → only CaptionBlock; + # postprocess folds Image+Caption. Stashing pending would glue this + # caption onto the *next* figure. + if parsed.kind in ("table", "listing"): + self._pending_caption = cap + else: + self._pending_caption = None + out.append(cap) + return out + + # Code / listing lines + if is_code_style(style): + self._flush_list(out) + if self._pending_caption and self._pending_caption.kind == "listing": + self._code_caption = self._pending_caption + self._pending_caption = None + self._code_buf.append(paragraph_raw(paragraph)) + return out + + # Images in paragraph + rids = iter_blip_rids(el) + if rids: + self._flush_code(out) + self._flush_list(out) + # Caption-before-image is rare; GOST puts caption under the picture + # (folded later). Only consume pending if it is explicitly a figure. + cap = self._pending_caption + cid = text_c = None + if cap and cap.kind == "figure": + cid = cap.unique_id + text_c = cap.text + self._pending_caption = None + for rid in rids: + rel = self.media.extract_rid(paragraph.part, rid) + if not rel: + self.log(f"Не удалось извлечь картинку {rid}") + continue + out.append( + ImageBlock( + rel_path=rel, + alt=text or "", + caption_id=cid, + caption_text=text_c, + ) + ) + cid = text_c = None # only first drawing gets pre-caption + return out + + # Bibliography + if is_bibliography_style(style) or parse_biblio_line(text): + self._flush_code(out) + self._flush_list(out) + biblio = parse_biblio_line(text) + if biblio: + key, body = biblio + out.append( + ParagraphBlock( + spans=[InlineSpan(text=f"[{key}]: {body}")], + style=style, + is_bibliography=True, + biblio_key=key, + ) + ) + return out + + # Lists (Word numPr or md2gost markers) + if paragraph_has_numpr(paragraph): + self._flush_code(out) + body = text + # strip auto number text if present at start — keep as item body + ordered = True + try: + # python-docx doesn't expose easily; assume numbered if numPr + m = re.match(r"^\d+\.\s*(.*)$", text) + if m: + body = m.group(1) + else: + # bullet + ordered = False + body = re.sub(r"^[–—\-•▪]\s*", "", text) + except Exception: + pass + if self._list_buf is not None and self._list_ordered == ordered: + self._list_buf.append(body) + else: + self._flush_list(out) + self._list_buf = [body] + self._list_ordered = ordered + return [] + + list_hit = look_like_list_item(text) + if list_hit and (paragraph.paragraph_format.left_indent or "\t" in (paragraph.text or "")): + ordered, _ok, body = list_hit + self._flush_code(out) + if self._list_buf is not None and self._list_ordered == ordered: + self._list_buf.append(body) + else: + self._flush_list(out) + self._list_buf = [body] + self._list_ordered = ordered + return [] + + # Plain paragraph (maybe with inline math) + self._flush_code(out) + self._flush_list(out) + if not text and not paragraph_has_omml(el): + return out + spans = _inline_spans(paragraph) + # If whole paragraph is only omml (block-ish), emit equation + oms = find_omml(el) + if oms and not text.replace(" ", ""): + latex = omml_to_latex(oms[0]) + if latex: + out.append(EquationBlock(latex=latex)) + return out + out.append(ParagraphBlock(spans=spans, style=style)) + self._pending_caption = None + return out + + def _handle_table(self, table: DocxTable) -> list[Block]: + out: list[Block] = [] + if not self._started: + # title-page layout tables — skip + return out + + if is_formula_table(table): + latex, number = formula_table_parts(table) + uid = None + if number: + uid = caption_unique_id("figure", number).replace("fig", "eq") + # better: eq + number + uid = "eq" + str(number).replace(".", "_") + out.append(EquationBlock(latex=latex or "?", number=number, unique_id=uid)) + self._pending_caption = None + return out + + landscape = _section_landscape(self.document, table._tbl) + rows, merges = table_to_grid(table) + if not rows: + return out + + cap = self._pending_caption + cid = text_c = None + if cap and cap.kind == "table" and not cap.is_continuation: + cid = cap.unique_id + text_c = cap.text + landscape = landscape or cap.landscape + self._pending_caption = None + elif cap and cap.is_continuation: + self._pending_caption = None + + out.append( + TableBlock( + rows=rows, + merges=merges, + caption_id=cid, + caption_text=text_c, + landscape=landscape, + ) + ) + return out + + @staticmethod + def _has_toc_field(p_element) -> bool: + for instr in p_element.findall(".//" + qn("w:instrText")): + raw = (instr.text or "").upper() + if "TOC" in raw: + return True + return False + + +def postprocess_blocks(blocks: list[Block]) -> list[Block]: + """Merge continuation tables/listings; fold figure captions under images; drop dup TOC.""" + result: list[Block] = [] + saw_toc = False + i = 0 + while i < len(blocks): + b = blocks[i] + if isinstance(b, TocPlaceholder): + if saw_toc: + i += 1 + continue + saw_toc = True + result.append(b) + i += 1 + continue + + if isinstance(b, CaptionBlock) and b.is_continuation: + kind = b.kind + i += 1 + while i < len(blocks) and isinstance(blocks[i], SkipNote): + i += 1 + if i >= len(blocks): + break + nxt = blocks[i] + prev = _last_block_of_type( + result, + TableBlock if kind == "table" else ListingBlock if kind == "listing" else None, + ) + if kind == "table" and isinstance(nxt, TableBlock) and isinstance(prev, TableBlock): + _merge_table_continuation(prev, nxt) + i += 1 + continue + if kind == "listing" and isinstance(nxt, ListingBlock) and isinstance(prev, ListingBlock): + prev.lines.extend(nxt.lines) + i += 1 + continue + result.append(nxt) + i += 1 + continue + + if isinstance(b, CaptionBlock) and not b.is_continuation: + result.append(b) + i += 1 + continue + + result.append(b) + i += 1 + + return _fold_figure_captions_after_images(result) + + +def _fold_figure_captions_after_images(blocks: list[Block]) -> list[Block]: + """ + In DOCX/GOST the caption is under the picture; in md2gost %id / title must + precede the image. Fold «Image + Caption(figure)» into one ImageBlock. + """ + out: list[Block] = [] + i = 0 + while i < len(blocks): + b = blocks[i] + nxt = blocks[i + 1] if i + 1 < len(blocks) else None + if ( + isinstance(b, ImageBlock) + and isinstance(nxt, CaptionBlock) + and nxt.kind == "figure" + and not nxt.is_continuation + ): + b.caption_id = b.caption_id or nxt.unique_id + b.caption_text = b.caption_text or nxt.text + out.append(b) + i += 2 + continue + # Drop a lone figure caption that duplicates the previous image's id + if ( + isinstance(b, CaptionBlock) + and b.kind == "figure" + and not b.is_continuation + and out + and isinstance(out[-1], ImageBlock) + and out[-1].caption_id == b.unique_id + ): + if not out[-1].caption_text and b.text: + out[-1].caption_text = b.text + i += 1 + continue + out.append(b) + i += 1 + return out + + +def _last_block_of_type(blocks: list[Block], typ) -> Block | None: + if typ is None: + return None + for b in reversed(blocks): + if isinstance(b, typ): + return b + return None + + +def _merge_table_continuation(prev: TableBlock, nxt: TableBlock) -> None: + """Append continuation rows; drop a repeated header row if Word duplicated it.""" + rows = list(nxt.rows or []) + merges = list(nxt.merges) if nxt.merges is not None else None + if prev.rows and rows and prev.rows[0] == rows[0]: + rows = rows[1:] + if merges is not None: + merges = merges[1:] + prev.rows.extend(rows) + if prev.merges is not None and merges is not None: + prev.merges.extend(merges) + elif prev.merges is None and merges is not None and prev.rows: + # keep merges only if both sides had them; otherwise drop + pass