v0.5.3
- Мелкие исправления увеличивающие работу конвертера
This commit is contained in:
@@ -1,13 +1,13 @@
|
|||||||
# This workflow will install Python dependencies, run tests and lint with a single version of Python
|
# Builds examples/example.md into a DOCX and uploads it as a build artifact.
|
||||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
|
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
|
||||||
|
|
||||||
name: Python application
|
name: Example document
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "main" ]
|
branches: [ "master" ]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "main" ]
|
branches: [ "master" ]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -18,15 +18,16 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python 3.11
|
- name: Set up Python 3.11
|
||||||
uses: actions/setup-python@v3
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
echo ttf-mscorefonts-installer msttcorefonts/accepted-mscorefonts-eula select true | sudo debconf-set-selections
|
echo ttf-mscorefonts-installer msttcorefonts/accepted-mscorefonts-eula select true | sudo debconf-set-selections
|
||||||
sudo apt-get install ttf-mscorefonts-installer
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y ttf-mscorefonts-installer
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
python -m venv venv
|
python -m venv venv
|
||||||
source venv/bin/activate
|
source venv/bin/activate
|
||||||
@@ -35,7 +36,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
source venv/bin/activate
|
source venv/bin/activate
|
||||||
python -m md2gost --syntax-highlighting examples/example.md
|
python -m md2gost --syntax-highlighting examples/example.md
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: example
|
name: example
|
||||||
path: example.docx
|
path: examples/example.docx
|
||||||
|
|||||||
+21
@@ -184,6 +184,8 @@ cython_debug/
|
|||||||
# md2gost runtime (created next to app / cwd on first run)
|
# md2gost runtime (created next to app / cwd on first run)
|
||||||
md2gost.schemes.json
|
md2gost.schemes.json
|
||||||
md2gost.include-cache.json
|
md2gost.include-cache.json
|
||||||
|
# Student profile: real ФИО/группа, must stay local (see md2gost.user.json.example)
|
||||||
|
md2gost.user.json
|
||||||
include-cache/
|
include-cache/
|
||||||
.md2gost-cache/
|
.md2gost-cache/
|
||||||
/build-fast
|
/build-fast
|
||||||
@@ -191,3 +193,22 @@ build-fast/md2gost.fast/xref-md2gost.fast.html
|
|||||||
build-fast/md2gost.fast/PYZ-00.pyz
|
build-fast/md2gost.fast/PYZ-00.pyz
|
||||||
build-fast/md2gost.fast/md2gost.pkg
|
build-fast/md2gost.fast/md2gost.pkg
|
||||||
build-fast/md2gost.fast/base_library.zip
|
build-fast/md2gost.fast/base_library.zip
|
||||||
|
|
||||||
|
# Dependency management is uv + uv.lock only
|
||||||
|
/poetry.lock
|
||||||
|
|
||||||
|
# Conversion output and scratch files kept out of the repo
|
||||||
|
/tmp_*.md
|
||||||
|
/tmp_*.docx
|
||||||
|
/tmp_*.png
|
||||||
|
/tmp_*.svg
|
||||||
|
/tmp_*.puml
|
||||||
|
/ReadMe.docx
|
||||||
|
md2gost/README.docx
|
||||||
|
/README.md1
|
||||||
|
/ad/
|
||||||
|
examples/example.docx
|
||||||
|
examples/example_out.docx
|
||||||
|
examples/toc_*.docx
|
||||||
|
tests/_idef0_preview*
|
||||||
|
latex/mirea/Settings/*.otf
|
||||||
|
|||||||
+11
-4
@@ -633,7 +633,7 @@ DOCX сам не умеет писать «Продолжение Таблицы
|
|||||||
| `test_md2fodt.py` | escape, каркас FODT, запись файла |
|
| `test_md2fodt.py` | escape, каркас FODT, запись файла |
|
||||||
| `test_md2latex.py` | longtable continuation, генерация проекта |
|
| `test_md2latex.py` | longtable continuation, генерация проекта |
|
||||||
| `test_word2md.py` | подписи, спецзаголовки, @ссылки, продолжения таблиц, DOCX→MD, round-trip |
|
| `test_word2md.py` | подписи, спецзаголовки, @ссылки, продолжения таблиц, DOCX→MD, round-trip |
|
||||||
| `extended_mardown/formula.py` | формулы (unittest) |
|
| `extended_markdown/formula.py` | формулы (unittest) |
|
||||||
|
|
||||||
Нет полного e2e «открыть Word и сравнить каждую страницу» в Linux CI (шрифты ставятся, Word COM — нет).
|
Нет полного e2e «открыть Word и сравнить каждую страницу» в Linux CI (шрифты ставятся, Word COM — нет).
|
||||||
|
|
||||||
@@ -660,10 +660,14 @@ DOCX сам не умеет писать «Продолжение Таблицы
|
|||||||
| `examples/example.md`, `examples/pis_custom.md` | образцы |
|
| `examples/example.md`, `examples/pis_custom.md` | образцы |
|
||||||
| `examples/md2gost.styles.json` | пример оверлея стилей |
|
| `examples/md2gost.styles.json` | пример оверлея стилей |
|
||||||
|
|
||||||
|
**Источник требований к оформлению** — [`docs/reference/method-mirea-2022.txt`](docs/reference/method-mirea-2022.txt): извлечённый текст методических указаний РТУ МИРЭА (Кириллина Ю.В. и др., 2022, на базе ГОСТ 7.32-2017). Значения в `md2gost/style_config.py` и правила checker'а сверяются по нему — при спорных местах смотреть номера строк этого файла. В exe не вшивается (это не пользовательская страница). Извлечение местами побито (потеряны начала строк), сверять по исходному PDF.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 11. Зависимости
|
## 11. Зависимости
|
||||||
|
|
||||||
|
Менеджер один: `[project]` в `pyproject.toml` + `uv.lock` (канонический lock-файл; `uv sync`). Poetry-метаданные, `poetry.lock` и `requirements.txt` удалены.
|
||||||
|
|
||||||
Обязательные (`pyproject.toml`): python-docx, marko, docxcompose, freetype-py, pillow, requests, latex2mathml, pygments, lxml.
|
Обязательные (`pyproject.toml`): python-docx, marko, docxcompose, freetype-py, pillow, requests, latex2mathml, pygments, lxml.
|
||||||
|
|
||||||
Опционально: pywin32 (Word COM), matplotlib (сборка exe / отладка), tkinterdnd2 (удобный DnD в GUI).
|
Опционально: pywin32 (Word COM), matplotlib (сборка exe / отладка), tkinterdnd2 (удобный DnD в GUI).
|
||||||
@@ -676,11 +680,14 @@ DOCX сам не умеет писать «Продолжение Таблицы
|
|||||||
|
|
||||||
Черновики и чужой код рядом с пакетами. Это **не** часть `pip install` / exe:
|
Черновики и чужой код рядом с пакетами. Это **не** часть `pip install` / exe:
|
||||||
|
|
||||||
- корень: `main.py`, `main12.py`, `mm2.py`…`mm5.py`, `asd.py`, `parser.py`, `font.py`, `_audit_tmp.py`, `tmp_listing_word.md`, `todo.txt`;
|
- корень: `todo.txt`, `delete_this.txt`;
|
||||||
- `ad/` — черновые отчёты;
|
- `ad/` — черновые отчёты (лежат локально, из git убраны);
|
||||||
- `_paco_extract/` — выгрузки методичек ПАЦО;
|
- `_paco_extract/` — выгрузки методичек ПАЦО;
|
||||||
- `Other_Code/` — справка OOXML, docx-js, скрипты pack/unpack/validate (не импортируется md2gost);
|
- `Other_Code/` — справка OOXML, docx-js, скрипты pack/unpack/validate (не импортируется md2gost);
|
||||||
- артефакты сборки `build-fast/`, кэши диаграмм.
|
- локальные артефакты: `tmp_*.{md,docx,png,svg,puml}`, `examples/*.docx`, `tests/_idef0_preview*`, `build-fast/`, кэши диаграмм — все в `.gitignore`;
|
||||||
|
- `md2gost.user.json` — профиль студента с реальными ФИО, в git не хранится (шаблон — `md2gost.user.json.example`).
|
||||||
|
|
||||||
|
Старые монолитные прототипы конвертера (`main.py`, `main12.py`, `mm2.py`…`mm5.py`, `asd.py`, корневые `parser.py` и `font.py`, `_audit_tmp.py`) удалены — их функциональность давно в `converter.py`, `renderer.py`, `style_config.py`. Не путать удалённый корневой `parser.py` с живым `md2gost/parser_.py` и корневой `font.py` с живым `md2gost/renderable/find_font.py`.
|
||||||
|
|
||||||
`todo.txt` сейчас: «оценки с текстом через сам Word, а не предугадывание» — частично закрыто режимом `word`.
|
`todo.txt` сейчас: «оценки с текстом через сам Word, а не предугадывание» — частично закрыто режимом `word`.
|
||||||
|
|
||||||
|
|||||||
-74
@@ -1,74 +0,0 @@
|
|||||||
# md2gost
|
|
||||||
|
|
||||||
Скрипт для генерации docx отчетов в соответствии с ГОСТ из markdown файла.
|
|
||||||
|
|
||||||
## Основные возможности
|
|
||||||
- Генерация отчета;
|
|
||||||
- ~~Добавление титульной страницы в формате docx~~(not yet);
|
|
||||||
- Генерация ~~интерактивного~~(not yet) содержания;
|
|
||||||
- Поддержка сквозной нумерации ~~и кросс-референсинга~~(not yet);
|
|
||||||
- Автоматическая расстановка картинок, продолжений таблиц и листингов и т.д.
|
|
||||||
|
|
||||||
## Пример
|
|
||||||
Markdown-файл: [example.md](https://github.com/witelokk/md2gost/blob/main/examples/example.md).
|
|
||||||
|
|
||||||
Сгенерированный файл в zip архиве (комманда `python -m md2gost --syntax-highlighting example.md`): [example.zip](https://nightly.link/witelokk/md2gost/workflows/example-generator/main/example.zip?h=f65c99d31a9379f44fcc6e923de4a735a271d5aa).
|
|
||||||
|
|
||||||
## Установка
|
|
||||||
```bash
|
|
||||||
pip install --upgrade git+https://github.com/witelokk/md2gost.git@main
|
|
||||||
```
|
|
||||||
|
|
||||||
Если ваша система приняла стандарт [PEP 668](https://peps.python.org/pep-0668/), используйте [pipx](https://pypa.github.io/pipx/):
|
|
||||||
```bash
|
|
||||||
pipx install git+https://github.com/witelokk/md2gost.git@main
|
|
||||||
```
|
|
||||||
|
|
||||||
## Использование
|
|
||||||
```
|
|
||||||
(python -m ) md2docx [-h] [-o OUTPUT] [-t TEMPLATE] [--syntax-highlighting | --no-syntax-highlighting] [--debug] filename
|
|
||||||
```
|
|
||||||
|
|
||||||
При отсутствии флага -o, сгенерированый отчет будет иметь имя с названием исходного файла и расширением .md.
|
|
||||||
|
|
||||||
## Фичи
|
|
||||||
|
|
||||||
### Подписи рисунков, листингов, таблиц
|
|
||||||
Рисунки:
|
|
||||||
```markdown
|
|
||||||

|
|
||||||
```
|
|
||||||
|
|
||||||
Таблицы:
|
|
||||||
```markdown
|
|
||||||
%uniquename Caption text
|
|
||||||
| a | b | c |
|
|
||||||
|---|---|---|
|
|
||||||
| a | b | c |
|
|
||||||
```
|
|
||||||
|
|
||||||
Листинги:
|
|
||||||
~~~markdown
|
|
||||||
%uniquename Caption text
|
|
||||||
```python
|
|
||||||
print("hello world")
|
|
||||||
```
|
|
||||||
~~~
|
|
||||||
|
|
||||||
`uniquename` - уникальное имя для ссылок.
|
|
||||||
|
|
||||||
### Заголовки для основных разделов
|
|
||||||
Для того чтобы у заголовка не было сквозной нумерации (например для заголовка Содержание), используйте
|
|
||||||
```markdown
|
|
||||||
# *Содержание
|
|
||||||
```
|
|
||||||
|
|
||||||
### Генерация содержания
|
|
||||||
```markdown
|
|
||||||
# *Содержание
|
|
||||||
[TOC]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Подсветка синтаксиса в листингах
|
|
||||||
Используйте флаг ```--syntax-highlighting```
|
|
||||||
|
|
||||||
BIN
Binary file not shown.
@@ -1,27 +0,0 @@
|
|||||||
from docx import Document
|
|
||||||
|
|
||||||
doc = Document(r"g:\Sync_File\Семестр 3\АИИР\КР\КР_out.docx")
|
|
||||||
|
|
||||||
print("=== biblio-ish ===")
|
|
||||||
for i, p in enumerate(doc.paragraphs):
|
|
||||||
t = p.text.strip()
|
|
||||||
style = p.style.name if p.style else None
|
|
||||||
if "ИСПОЛЬЗОВАНН" in t.upper():
|
|
||||||
print(i, style, t[:80])
|
|
||||||
if style == "Bibliography" or (t[:2] in {f"{n}." for n in range(1, 10)} and len(t) > 50):
|
|
||||||
print(i, style, t[:100])
|
|
||||||
|
|
||||||
print("=== double spaces in captions ===")
|
|
||||||
for p in doc.paragraphs:
|
|
||||||
if p.style and "Caption" in (p.style.name or "") and " " in p.text:
|
|
||||||
print(repr(p.text[:90]))
|
|
||||||
|
|
||||||
print("=== sections ===")
|
|
||||||
for i, s in enumerate(doc.sections):
|
|
||||||
has_page = "PAGE" in s.footer._element.xml
|
|
||||||
print(i, "PAGE field", has_page)
|
|
||||||
|
|
||||||
print("=== Heading 1 ===")
|
|
||||||
for p in doc.paragraphs:
|
|
||||||
if p.style and p.style.name == "Heading 1":
|
|
||||||
print(repr(p.text[:100]))
|
|
||||||
-115
@@ -1,115 +0,0 @@
|
|||||||
import pypandoc
|
|
||||||
from docx import Document
|
|
||||||
from docx.shared import Pt, Cm
|
|
||||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
|
||||||
from docx.enum.style import WD_STYLE_TYPE
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def apply_custom_formatting(doc):
|
|
||||||
"""Применяет дополнительное форматирование, которое не может быть обработано Pandoc."""
|
|
||||||
|
|
||||||
# Нумерация страниц (простой пример)
|
|
||||||
# Более сложные колонтитулы лучше настроить в reference.docx
|
|
||||||
for section in doc.sections:
|
|
||||||
section.footer.paragraphs[0].text = "\t" # Чтобы номер был по центру или справа, настройте табуляцию
|
|
||||||
|
|
||||||
# Добавление рамок к листингам кода, если они не были применены стилем
|
|
||||||
for paragraph in doc.paragraphs:
|
|
||||||
if 'листинг' in paragraph.text.lower() or 'listing' in paragraph.text.lower():
|
|
||||||
# Здесь можно применить стиль, который вы создали, например 'CodeListing'
|
|
||||||
# paragraph.style = 'CodeListing'
|
|
||||||
pass # Логика для добавления границ, если нужно
|
|
||||||
|
|
||||||
|
|
||||||
def markdown_to_docx(md_file, docx_file, reference_doc):
|
|
||||||
"""
|
|
||||||
Основная функция для конвертации Markdown в DOCX.
|
|
||||||
"""
|
|
||||||
# Шаг 1: Используем Pandoc для основной конвертации
|
|
||||||
pypandoc.convert_file(
|
|
||||||
md_file,
|
|
||||||
'docx',
|
|
||||||
outputfile=docx_file,
|
|
||||||
extra_args=[f'--reference-doc={reference_doc}', '--toc'] # --toc для создания оглавления
|
|
||||||
)
|
|
||||||
|
|
||||||
# Шаг 2: Открываем созданный документ с помощью python-docx для дальнейшей обработки
|
|
||||||
doc = Document(docx_file)
|
|
||||||
|
|
||||||
# Шаг 3: Вставляем титульный лист в начало документа
|
|
||||||
# Создаем новый пустой документ для титульного листа
|
|
||||||
title_doc = Document()
|
|
||||||
# Добавляем содержимое основного документа после титульного листа
|
|
||||||
for element in doc.element.body:
|
|
||||||
title_doc.element.body.append(element)
|
|
||||||
|
|
||||||
doc = title_doc
|
|
||||||
|
|
||||||
# Шаг 4: Применяем дополнительное форматирование
|
|
||||||
apply_custom_formatting(doc)
|
|
||||||
|
|
||||||
# Шаг 5: Сохраняем финальный документ
|
|
||||||
doc.save(docx_file)
|
|
||||||
print(f"Документ '{docx_file}' успешно создан.")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
# --- НАСТРОЙКИ ---
|
|
||||||
markdown_input = 'my_report.md' # Имя вашего входного Markdown файла
|
|
||||||
docx_output = 'my_report.docx' # Имя выходного DOCX файла
|
|
||||||
reference_docx = 'samost1.docx' # Имя вашего эталонного документа
|
|
||||||
|
|
||||||
|
|
||||||
# Создаем пример Markdown файла для демонстрации
|
|
||||||
markdown_content = """
|
|
||||||
# Введение
|
|
||||||
|
|
||||||
Это введение в мою курсовую работу. Текст должен быть отформатирован с использованием стиля "Обычный".
|
|
||||||
|
|
||||||
# 1. Структура учебной работы
|
|
||||||
|
|
||||||
Здесь описывается структура работы.
|
|
||||||
|
|
||||||
## 1.1 Титульный лист
|
|
||||||
|
|
||||||
Титульный лист - это первая страница работы.
|
|
||||||
|
|
||||||
## 1.2 Содержание
|
|
||||||
|
|
||||||
Содержание включает в себя перечень всех разделов.
|
|
||||||
|
|
||||||
# 2. Оформление текста документа
|
|
||||||
|
|
||||||
Здесь описываются правила оформления текста.
|
|
||||||
|
|
||||||
- Первое правило
|
|
||||||
- Второе правило
|
|
||||||
- Третье правило
|
|
||||||
|
|
||||||
1. Нумерованный список 1
|
|
||||||
2. Нумерованный список 2
|
|
||||||
|
|
||||||
Листинг 1.1 – Пример кода
|
|
||||||
|
|
||||||
` ` `python
|
|
||||||
def hello_world():
|
|
||||||
print("Hello, World!")
|
|
||||||
` ` `
|
|
||||||
|
|
||||||
Таблица 1.1 - Пример таблицы
|
|
||||||
|
|
||||||
| Заголовок 1 | Заголовок 2 |
|
|
||||||
|-------------|-------------|
|
|
||||||
| Ячейка 1 | Ячейка 2 |
|
|
||||||
| Ячейка 3 | Ячейка 4 |
|
|
||||||
|
|
||||||
# Заключение
|
|
||||||
|
|
||||||
Это заключение.
|
|
||||||
"""
|
|
||||||
with open(markdown_input, 'w', encoding='utf-8') as f:
|
|
||||||
f.write(markdown_content)
|
|
||||||
|
|
||||||
# --- ЗАПУСК КОНВЕРТЕРА ---
|
|
||||||
markdown_to_docx(markdown_input, docx_output, reference_docx)
|
|
||||||
Binary file not shown.
@@ -1,45 +0,0 @@
|
|||||||
|
|
||||||
# Введение
|
|
||||||
|
|
||||||
Это введение в мою курсовую работу. Текст должен быть отформатирован с использованием стиля "Обычный".
|
|
||||||
|
|
||||||
# 1. Структура учебной работы
|
|
||||||
|
|
||||||
Здесь описывается структура работы.
|
|
||||||
|
|
||||||
## 1.1 Титульный лист
|
|
||||||
|
|
||||||
Титульный лист - это первая страница работы.
|
|
||||||
|
|
||||||
## 1.2 Содержание
|
|
||||||
|
|
||||||
Содержание включает в себя перечень всех разделов.
|
|
||||||
|
|
||||||
# 2. Оформление текста документа
|
|
||||||
|
|
||||||
Здесь описываются правила оформления текста.
|
|
||||||
|
|
||||||
- Первое правило
|
|
||||||
- Второе правило
|
|
||||||
- Третье правило
|
|
||||||
|
|
||||||
1. Нумерованный список 1
|
|
||||||
2. Нумерованный список 2
|
|
||||||
|
|
||||||
Листинг 1.1 – Пример кода
|
|
||||||
|
|
||||||
` ` `python
|
|
||||||
def hello_world():
|
|
||||||
print("Hello, World!")
|
|
||||||
` ` `
|
|
||||||
|
|
||||||
Таблица 1.1 - Пример таблицы
|
|
||||||
|
|
||||||
| Заголовок 1 | Заголовок 2 |
|
|
||||||
|-------------|-------------|
|
|
||||||
| Ячейка 1 | Ячейка 2 |
|
|
||||||
| Ячейка 3 | Ячейка 4 |
|
|
||||||
|
|
||||||
# Заключение
|
|
||||||
|
|
||||||
Это заключение.
|
|
||||||
Binary file not shown.
+147
@@ -0,0 +1,147 @@
|
|||||||
|
# ai/ — промпты и документация для нейросетей
|
||||||
|
|
||||||
|
Материалы md2gost, переписанные под работу с языковыми моделями.
|
||||||
|
|
||||||
|
**Главное правило: файл из `ai/prompts/` — это целиком промпт.** В нём нет ни заголовка «скопируйте текст ниже», ни разделителей, ни инструкций для человека. Открыл файл, выделил всё, вставил в модель — работает. Инструкции для человека живут только в этом README.
|
||||||
|
|
||||||
|
Файлы из `ai/context/` — справочники: их дают модели как дополнительный контекст, когда нужна точность в узком месте. Файлы из `ai/examples/` — эталонные документы, проходящие проверку `--check` без замечаний; их дают как образец.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Что где лежит
|
||||||
|
|
||||||
|
### `ai/prompts/` — готовые к вставке промпты
|
||||||
|
|
||||||
|
| Файл | Когда брать | Размер |
|
||||||
|
|------|-------------|--------|
|
||||||
|
| [`core-md2gost.md`](prompts/core-md2gost.md) | нужен просто markdown в диалекте md2gost, без привязки к типу учебной работы | 12 тыс. знаков |
|
||||||
|
| [`report-mirea.md`](prompts/report-mirea.md) | отчёт по практике, курсовая, ВКР (`practice`, `coursework`, `vkr`) | 12 тыс. знаков |
|
||||||
|
| [`report-apid.md`](prompts/report-apid.md) | курсовая «Архитектура приложений и данных» (`APID_coursework`) | 10 тыс. знаков |
|
||||||
|
| [`report-pis.md`](prompts/report-pis.md) | итоговый отчёт по практическим работам (`PIS_custom`) | 7 тыс. знаков |
|
||||||
|
| [`voice-human.md`](prompts/voice-human.md) | добавка к любому из промптов выше: живой текст про предмет, а не отчёт модели о сдаче задания | 5 тыс. знаков |
|
||||||
|
| [`review-fix.md`](prompts/review-fix.md) | есть готовый `.md`, надо починить его под ТЗ (в том числе по выводу `--check`) | 8 тыс. знаков |
|
||||||
|
| [`from-word.md`](prompts/from-word.md) | есть результат импорта `word2md`, надо довести до диалекта | 6 тыс. знаков |
|
||||||
|
|
||||||
|
Промпты самодостаточны: правила диалекта в них повторяются намеренно, чтобы каждый работал в одиночку.
|
||||||
|
|
||||||
|
### `ai/context/` — справочники для догрузки
|
||||||
|
|
||||||
|
| Файл | О чём | Размер |
|
||||||
|
|------|-------|--------|
|
||||||
|
| [`syntax.md`](context/syntax.md) | полная спецификация диалекта: регулярные выражения, приоритеты, подводные камни | 16 тыс. знаков |
|
||||||
|
| [`diagrams.md`](context/diagrams.md) | все нотации схем с готовыми каркасами: PlantUML, C4, Mermaid, IDEF0, DFD | 16 тыс. знаков |
|
||||||
|
| [`archimate.md`](context/archimate.md) | ArchiMate 3.2: макросы элементов и связей, направления, viewpoints, шаблоны | 22 тыс. знаков |
|
||||||
|
| [`checker.md`](context/checker.md) | каталог замечаний `--check` с текстами сообщений и способами починки | 10 тыс. знаков |
|
||||||
|
| [`cli.md`](context/cli.md) | все ключи запуска, профили типов, конфигурационные файлы, оформление | 12 тыс. знаков |
|
||||||
|
|
||||||
|
### `ai/examples/` — эталоны
|
||||||
|
|
||||||
|
| Файл | Тип | Что демонстрирует |
|
||||||
|
|------|-----|-------------------|
|
||||||
|
| [`coursework.md`](examples/coursework.md) | `coursework` | полная структура, IDEF0, C4, ER-диаграмма, объединение ячеек, формула, листинг, два приложения, шесть источников |
|
||||||
|
| [`pis-report.md`](examples/pis-report.md) | `PIS_custom` | четыре практические работы, DFD, IDEF0, сквозные метки, расчёт нагрузки |
|
||||||
|
|
||||||
|
Оба файла проверены: `--check-only` не даёт ни одного замечания, сборка DOCX проходит на локальном рендере без интернета.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Рецепты
|
||||||
|
|
||||||
|
Что вставлять в модель под конкретную задачу.
|
||||||
|
|
||||||
|
| Задача | Что даёшь модели |
|
||||||
|
|--------|------------------|
|
||||||
|
| Сгенерировать курсовую или отчёт по практике | `prompts/report-mirea.md` → вторым сообщением `prompts/voice-human.md` → третьим сообщением входные данные |
|
||||||
|
| То же, но нужен упор на схемы | добавь `context/diagrams.md` перед входными данными |
|
||||||
|
| Курсовая АПИД | `prompts/report-apid.md` + `prompts/voice-human.md` + при необходимости `context/archimate.md` |
|
||||||
|
| Отчёт по практическим работам ПИС | `prompts/report-pis.md` + входные данные |
|
||||||
|
| Дописать раздел в существующий отчёт | `prompts/core-md2gost.md` + `prompts/voice-human.md` + текущий `.md` целиком + что дописать |
|
||||||
|
| Починить файл по замечаниям проверки | `prompts/review-fix.md` + вывод `--check` + `.md` целиком |
|
||||||
|
| Довести импорт из Word | `prompts/from-word.md` + результат `word2md` |
|
||||||
|
| Нарисовать одну схему | `context/diagrams.md` (или `context/archimate.md`) + описание того, что надо изобразить |
|
||||||
|
| Разобраться, почему конвертер ругается | `context/checker.md` + текст ошибки |
|
||||||
|
| Модель путается в синтаксисе | добавь `context/syntax.md` |
|
||||||
|
| Нужен образец «как должно выглядеть» | добавь `examples/coursework.md` |
|
||||||
|
|
||||||
|
Если контекст модели ограничен, порядок приоритета такой: промпт → входные данные → эталон → справочник. Справочник — первое, чем можно пожертвовать.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Шаблон входных данных
|
||||||
|
|
||||||
|
Следующим сообщением после промпта дай модели заполненный блок:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Тип работы: coursework
|
||||||
|
Тема: Разработка модуля учёта заявок на техническое обслуживание
|
||||||
|
Год сдачи: 2026
|
||||||
|
Дисциплина: Проектирование информационных систем
|
||||||
|
|
||||||
|
Объект: процесс приёма и исполнения заявок на обслуживание оборудования
|
||||||
|
Что уже известно:
|
||||||
|
- склад обрабатывает около 400 отгрузок в сутки
|
||||||
|
- заявки принимаются по телефону, фиксируются в электронных таблицах
|
||||||
|
- нужна интеграция со справочником оборудования по REST
|
||||||
|
|
||||||
|
Требования руководителя:
|
||||||
|
- обязательны диаграмма прецедентов и модель данных
|
||||||
|
- три раздела основной части
|
||||||
|
|
||||||
|
Литература: если есть свой список — приведи его здесь; иначе модель подберёт сама
|
||||||
|
Черновик: приложи имеющийся текст, если он есть
|
||||||
|
```
|
||||||
|
|
||||||
|
Чем конкретнее «что уже известно», тем меньше выдумок в тексте. Числа, названия и ограничения — самое ценное, что можно дать.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Полный цикл работы
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Сохранить ответ модели как report.md
|
||||||
|
|
||||||
|
# 2. Проверить по ТЗ
|
||||||
|
python -m md2gost report.md --check-only --type coursework
|
||||||
|
|
||||||
|
# 3. Если есть замечания — отдать их модели вместе с файлом
|
||||||
|
# и промптом ai/prompts/review-fix.md, сохранить результат, вернуться к шагу 2
|
||||||
|
|
||||||
|
# 4. Собрать документ
|
||||||
|
python -m md2gost report.md -o report.docx --type coursework --check --strict
|
||||||
|
|
||||||
|
# 5. Проверить вёрстку готового файла (Windows + Word + pywin32)
|
||||||
|
python -m md2gost report.docx --check-pages
|
||||||
|
```
|
||||||
|
|
||||||
|
Автотитул при необходимости: положить рядом `info_conv.yaml`, добавить в начало `.md` блок ` ```title ` с номером работы и годом, запустить с `--auto-title --student "…" --group "…"`. Подробности — в [`context/cli.md`](context/cli.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Куда вставлять промпт
|
||||||
|
|
||||||
|
| Инструмент | Куда |
|
||||||
|
|------------|------|
|
||||||
|
| ChatGPT, Claude | системная инструкция проекта либо первое сообщение диалога |
|
||||||
|
| Cursor | правила для ИИ, либо первое сообщение агенту |
|
||||||
|
| GitHub Copilot Chat | контекст чата |
|
||||||
|
| API | роль `system` |
|
||||||
|
|
||||||
|
Промпты рассчитаны на модель с рассуждением и длинным контекстом: они содержат чек-листы самопроверки, которые слабые модели игнорируют. Если результат не проходит `--check` с первого раза — это нормальный рабочий цикл, для него и сделан `review-fix.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Что модель делать не должна
|
||||||
|
|
||||||
|
Проверка `--check` не ловит фактические ошибки и выдуманные источники. Перед сдачей обязательно вычитай:
|
||||||
|
|
||||||
|
- **список литературы** — модели склонны придумывать издания, ISBN и число страниц. Промпты запрещают это прямо, но проверять всё равно нужно;
|
||||||
|
- **числа и названия** — если ты их не давал, модель их сочинила;
|
||||||
|
- **соответствие теме** — структура может быть безупречной при пустом содержании.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Отношение к остальному репозиторию
|
||||||
|
|
||||||
|
Каталоги `docs/` и `prompts/` в корне репозитория остаются как есть: на них ссылается встроенная справка приложения (**Справка → Документация**, **Справка → Промпт для ИИ**). Каталог `ai/` — самостоятельный набор, переписанный под подачу в модели, и ничего в приложении не ломает.
|
||||||
|
|
||||||
|
Источник истины по возможностям конвертера — код в `md2gost/`; справочники в `ai/context/` собраны по нему, а не по старой документации.
|
||||||
@@ -0,0 +1,514 @@
|
|||||||
|
# ArchiMate 3.2 для md2gost — рабочий справочник
|
||||||
|
|
||||||
|
Как корректно генерировать диаграммы ArchiMate® 3.2 в PlantUML для отчётов md2gost. Версия языка — только **3.2**. ArchiMate® — товарный знак The Open Group; этот текст — учебная выжимка, не копия спецификации.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Как рисовать
|
||||||
|
|
||||||
|
Оградка ` ```uml-archimate ` или ` ```archimate `. В теле — **только макросы**: ни `@startuml`, ни `@enduml`, ни `!include` (их подставит схема).
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
Слоистая архитектура портала приведена на @Рисунок:arch_layered.
|
||||||
|
|
||||||
|
%arch_layered Слоистая архитектура портала +landscape
|
||||||
|
|
||||||
|
```uml-archimate
|
||||||
|
LAYOUT_TOP_DOWN()
|
||||||
|
|
||||||
|
Business_Actor(student, "Студент")
|
||||||
|
Business_Process(enroll, "Подача заявки")
|
||||||
|
Application_Service(portalSvc, "Сервис приёма заявок")
|
||||||
|
Application_Component(portal, "Веб-портал")
|
||||||
|
|
||||||
|
Rel_Assignment(student, enroll)
|
||||||
|
Rel_Serving(portalSvc, enroll)
|
||||||
|
Rel_Realization(portal, portalSvc)
|
||||||
|
```
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Широкие схемы (Layered, Application Cooperation) почти всегда с `+landscape`. Флаг `+listing` добавит исходник схемы отдельным Листингом.
|
||||||
|
|
||||||
|
Формат макроса: `Категория_Элемент(alias, "Подпись")`. Alias — латиница (`student`, `crm`); нельзя `end`, `start`, `group`. Подписи русские, в кавычках; длинные переносятся через `\n`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Каркас языка
|
||||||
|
|
||||||
|
**Три core-слоя:**
|
||||||
|
|
||||||
|
| Слой | Что показывает |
|
||||||
|
|------|----------------|
|
||||||
|
| Business | бизнес-сервисы для клиентов, процессы и акторы, которые их реализуют |
|
||||||
|
| Application | прикладные сервисы и приложения, поддерживающие бизнес |
|
||||||
|
| Technology | обработка, хранение, сети, а также физический мир |
|
||||||
|
|
||||||
|
Плюс слой Strategy, слой Implementation & Migration и аспект Motivation.
|
||||||
|
|
||||||
|
**Четыре аспекта:**
|
||||||
|
|
||||||
|
| Аспект | Роль | Примеры |
|
||||||
|
|--------|------|---------|
|
||||||
|
| Active structure | кто действует | Actor, Role, Component, Node, Device |
|
||||||
|
| Behavior | что делается | Process, Function, Interaction, Service, Event |
|
||||||
|
| Passive structure | над чем работают | Business Object, Data Object, Artifact |
|
||||||
|
| Motivation | зачем | Stakeholder, Driver, Goal, Requirement |
|
||||||
|
|
||||||
|
**Каноническая цепочка внутри слоя:**
|
||||||
|
|
||||||
|
```text
|
||||||
|
Active structure —Assignment→ internal behavior (Process / Function)
|
||||||
|
internal behavior —Realization→ Service (внешнее поведение)
|
||||||
|
Service —Serving→ потребитель (Role / Process / Component)
|
||||||
|
behavior —Access→ passive structure
|
||||||
|
```
|
||||||
|
|
||||||
|
**Два межслойных моста:** Serving (сервис предоставляет функциональность потребителю) и Realization (более конкретное реализует более абстрактное).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Элементы → макросы
|
||||||
|
|
||||||
|
### Motivation
|
||||||
|
|
||||||
|
| Элемент | Макрос | Определение |
|
||||||
|
|---------|--------|-------------|
|
||||||
|
| Stakeholder | `Motivation_Stakeholder` | роль лица или организации, заинтересованной в эффектах архитектуры |
|
||||||
|
| Driver | `Motivation_Driver` | условие, побуждающее ставить цели и вносить изменения |
|
||||||
|
| Assessment | `Motivation_Assessment` | результат анализа положения дел относительно driver |
|
||||||
|
| Goal | `Motivation_Goal` | высокоуровневое намерение, желаемое состояние |
|
||||||
|
| Outcome | `Motivation_Outcome` | конечный результат, эффект |
|
||||||
|
| Principle | `Motivation_Principle` | общее свойство, применимое к любой системе в контексте |
|
||||||
|
| Requirement | `Motivation_Requirement` | требуемое свойство конкретной системы |
|
||||||
|
| Constraint | `Motivation_Constraint` | ограничение на архитектуру или внедрение |
|
||||||
|
| Meaning | `Motivation_Meaning` | интерпретация концепта в контексте |
|
||||||
|
| Value | `Motivation_Value` | относительная ценность концепта |
|
||||||
|
|
||||||
|
### Strategy
|
||||||
|
|
||||||
|
| Элемент | Макрос | Определение |
|
||||||
|
|---------|--------|-------------|
|
||||||
|
| Resource | `Strategy_Resource` | актив, которым владеют или управляют |
|
||||||
|
| Capability | `Strategy_Capability` | способность активной структуры |
|
||||||
|
| Value Stream | `Strategy_ValueStream` | последовательность активностей, создающая результат для клиента |
|
||||||
|
| Course of Action | `Strategy_CourseOfAction` | план настройки capabilities и resources под цель |
|
||||||
|
|
||||||
|
### Business
|
||||||
|
|
||||||
|
| Элемент | Макрос | Аспект | Определение |
|
||||||
|
|---------|--------|--------|-------------|
|
||||||
|
| Business Actor | `Business_Actor` | active | сущность, способная выполнять поведение |
|
||||||
|
| Business Role | `Business_Role` | active | ответственность за поведение |
|
||||||
|
| Business Collaboration | `Business_Collaboration` | active | агрегат двух и более активных структур |
|
||||||
|
| Business Interface | `Business_Interface` | active | точка доступа к бизнес-сервису |
|
||||||
|
| Business Process | `Business_Process` | behavior | последовательность поведения к результату |
|
||||||
|
| Business Function | `Business_Function` | behavior | набор поведения по компетенциям и ресурсам |
|
||||||
|
| Business Interaction | `Business_Interaction` | behavior | коллективное поведение участников |
|
||||||
|
| Business Event | `Business_Event` | behavior | изменение состояния на бизнес-уровне |
|
||||||
|
| Business Service | `Business_Service` | behavior | явно определённое поведение, доступное среде |
|
||||||
|
| Business Object | `Business_Object` | passive | концепт предметной области |
|
||||||
|
| Contract | `Business_Contract` | passive | соглашение провайдера и потребителя |
|
||||||
|
| Representation | `Business_Representation` | passive | форма представления информации объекта |
|
||||||
|
| Product | `Business_Product` | composite | набор сервисов и объектов вместе с контрактом |
|
||||||
|
|
||||||
|
### Application
|
||||||
|
|
||||||
|
| Элемент | Макрос | Аспект | Определение |
|
||||||
|
|---------|--------|--------|-------------|
|
||||||
|
| Application Component | `Application_Component` | active | модульная инкапсуляция прикладной функциональности |
|
||||||
|
| Application Collaboration | `Application_Collaboration` | active | агрегат прикладных активных структур |
|
||||||
|
| Application Interface | `Application_Interface` | active | точка доступа к прикладным сервисам |
|
||||||
|
| Application Function | `Application_Function` | behavior | автоматизированное поведение компонента |
|
||||||
|
| Application Interaction | `Application_Interaction` | behavior | коллективное поведение компонентов |
|
||||||
|
| Application Process | `Application_Process` | behavior | последовательность прикладного поведения |
|
||||||
|
| Application Event | `Application_Event` | behavior | изменение состояния приложения |
|
||||||
|
| Application Service | `Application_Service` | behavior | внешне видимое прикладное поведение |
|
||||||
|
| Data Object | `Application_DataObject` | passive | данные, структурированные для обработки |
|
||||||
|
|
||||||
|
### Technology и Physical
|
||||||
|
|
||||||
|
| Элемент | Макрос | Определение |
|
||||||
|
|---------|--------|-------------|
|
||||||
|
| Node | `Technology_Node` | вычислительный или физический ресурс, хостящий другие |
|
||||||
|
| Device | `Technology_Device` | физический ИТ-ресурс |
|
||||||
|
| System Software | `Technology_SystemSoftware` | ПО среды исполнения или хранения |
|
||||||
|
| Technology Collaboration | `Technology_Collaboration` | агрегат технологических активных структур |
|
||||||
|
| Technology Interface | `Technology_Interface` | точка доступа к технологическим сервисам |
|
||||||
|
| Path | `Technology_Path` | связь между технологическими структурами для обмена |
|
||||||
|
| Communication Network | `Technology_CommunicationNetwork` | структуры передачи данных |
|
||||||
|
| Technology Function | `Technology_Function` | набор технологического поведения |
|
||||||
|
| Technology Process | `Technology_Process` | последовательность технологического поведения |
|
||||||
|
| Technology Interaction | `Technology_Interaction` | коллективное технологическое поведение |
|
||||||
|
| Technology Event | `Technology_Event` | изменение технологического состояния |
|
||||||
|
| Technology Service | `Technology_Service` | внешне видимое технологическое поведение |
|
||||||
|
| Artifact | `Technology_Artifact` | данные или файл, используемый при разработке и эксплуатации |
|
||||||
|
| Equipment | `Physical_Equipment` | физические машины и инструменты |
|
||||||
|
| Facility | `Physical_Facility` | физическая структура или окружение |
|
||||||
|
| Distribution Network | `Physical_DistributionNetwork` | сеть транспортировки материи или энергии |
|
||||||
|
| Material | `Physical_Material` | осязаемая материя или энергия |
|
||||||
|
|
||||||
|
В 3.2 Device, System Software, Facility и Equipment — **не** подтипы Node, а технологические активные структуры, связанные с Node композицией или агрегацией.
|
||||||
|
|
||||||
|
### Implementation & Migration
|
||||||
|
|
||||||
|
| Элемент | Макрос | Определение |
|
||||||
|
|---------|--------|-------------|
|
||||||
|
| Work Package | `Implementation_WorkPackage` | серия действий с результатом в сроках и ресурсах |
|
||||||
|
| Deliverable | `Implementation_Deliverable` | определённый результат work package |
|
||||||
|
| Implementation Event | `Implementation_Event` | изменение состояния при внедрении |
|
||||||
|
| Plateau | `Implementation_Plateau` | стабильное состояние архитектуры на период |
|
||||||
|
| Gap | `Implementation_Gap` | различие между двумя plateau |
|
||||||
|
|
||||||
|
### Composite и Junction
|
||||||
|
|
||||||
|
| Элемент | Макрос |
|
||||||
|
|---------|--------|
|
||||||
|
| Grouping | `Grouping` / `Other_Grouping` / `Group` |
|
||||||
|
| Location | `Other_Location` / `Business_Location` |
|
||||||
|
| And Junction | `Junction_And` |
|
||||||
|
| Or Junction | `Junction_Or` |
|
||||||
|
|
||||||
|
Grouping и Junction допустимы в любом viewpoint.
|
||||||
|
|
||||||
|
### Какой элемент поведения выбрать
|
||||||
|
|
||||||
|
| Нужно показать | Берётся |
|
||||||
|
|----------------|---------|
|
||||||
|
| последовательность шагов к результату | `*_Process` |
|
||||||
|
| группу поведения по компетенциям и ресурсам | `*_Function` |
|
||||||
|
| поведение двух и более участников | `*_Interaction` |
|
||||||
|
| внешне видимое поведение, чёрный ящик | `*_Service` |
|
||||||
|
| момент или смену состояния | `*_Event` |
|
||||||
|
|
||||||
|
Слой задаётся типом элемента, а не подписью: `Business_Process` ≠ `Application_Process` ≠ `Technology_Process`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Связи
|
||||||
|
|
||||||
|
| Тип | Класс | Смысл | Макрос | Нотация |
|
||||||
|
|-----|-------|-------|--------|---------|
|
||||||
|
| Composition | structural | целое состоит из частей, часть не живёт отдельно | `Rel_Composition` | `*--` |
|
||||||
|
| Aggregation | structural | целое объединяет части, части живут отдельно | `Rel_Aggregation` | `o--` |
|
||||||
|
| Assignment | structural | ответственность, исполнение, размещение | `Rel_Assignment` | `@-->>` |
|
||||||
|
| Realization | structural | конкретное реализует абстрактное | `Rel_Realization` | `~~\|>` |
|
||||||
|
| Serving | dependency | предоставляет функциональность другому | `Rel_Serving` | `-->` |
|
||||||
|
| Access | dependency | поведение работает с пассивным элементом | `Rel_Access`, `_r`, `_w`, `_rw` | `~~` |
|
||||||
|
| Influence | dependency | влияет на достижение motivation-элемента | `Rel_Influence` | `..>` |
|
||||||
|
| Triggering | dynamic | временная или причинная активация | `Rel_Triggering` | `-->>` |
|
||||||
|
| Flow | dynamic | передача чего-либо | `Rel_Flow` | `..>>` |
|
||||||
|
| Specialization | other | является разновидностью | `Rel_Specialization` | `--\|>` |
|
||||||
|
| Association | other | неспецифичная связь | `Rel_Association` | `--` |
|
||||||
|
|
||||||
|
Направление раскладки: суффиксы `_Up`, `_Down`, `_Left`, `_Right` — например `Rel_Serving_Up(svc, proc)`.
|
||||||
|
|
||||||
|
### Направления связей — где чаще всего ошибаются
|
||||||
|
|
||||||
|
| Связь | Откуда → куда |
|
||||||
|
|-------|---------------|
|
||||||
|
| Serving | поставщик (обычно Service) → **потребитель** |
|
||||||
|
| Realization | более конкретное → более абстрактное |
|
||||||
|
| Composition, Aggregation | целое → часть |
|
||||||
|
| Assignment | активная структура → поведение |
|
||||||
|
| Access | поведение → **пассивный** элемент, вид read / write / read-write |
|
||||||
|
| Triggering | предшественник → следующий |
|
||||||
|
| Flow | источник → приёмник |
|
||||||
|
| Influence | влияющий → motivation-элемент, сила в подписи (`++`, `+`, `0`, `-`, `--`) |
|
||||||
|
| Specialization | специализация → общий тип |
|
||||||
|
|
||||||
|
### Правила метамодели
|
||||||
|
|
||||||
|
- Элемент может композировать или агрегировать элементы того же класса.
|
||||||
|
- Внутренние поведения одного слоя могут агрегировать друг друга (Function агрегирует Process).
|
||||||
|
- Collaboration ведёт себя как соответствующая внутренняя активная структура.
|
||||||
|
- Stakeholder связывается Association с любым элементом; остальные motivation-элементы влияют друг на друга через Influence.
|
||||||
|
- Association формально возможна между любыми классами, но применяется только когда нет более точного типа.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Канонические цепочки
|
||||||
|
|
||||||
|
```text
|
||||||
|
# Внутри слоя Business
|
||||||
|
Business_Actor / Business_Role Rel_Assignment Business_Process / Function
|
||||||
|
Business_Process Rel_Realization Business_Service
|
||||||
|
Business_Process Rel_Access_* Business_Object
|
||||||
|
Business_Event Rel_Triggering Business_Process
|
||||||
|
|
||||||
|
# Внутри слоя Application
|
||||||
|
Application_Component Rel_Assignment Application_Function / Process
|
||||||
|
Application_Function Rel_Realization Application_Service
|
||||||
|
Application_Service Rel_Serving Business_Process / Business_Role
|
||||||
|
Application_Component Rel_Access_* Application_DataObject
|
||||||
|
Application_Component Rel_Composition Application_Component
|
||||||
|
|
||||||
|
# Технологии и данные
|
||||||
|
Technology_Node Rel_Composition Technology_Device / SystemSoftware
|
||||||
|
Technology_* Rel_Assignment Technology_Function / Process
|
||||||
|
Technology_Function Rel_Realization Technology_Service
|
||||||
|
Technology_Service Rel_Serving Application_Component
|
||||||
|
Technology_Artifact Rel_Realization Application_DataObject
|
||||||
|
Application_DataObject Rel_Realization Business_Object
|
||||||
|
|
||||||
|
# Стратегия и мотивация
|
||||||
|
Business_Process / App_Service Rel_Realization Strategy_Capability
|
||||||
|
Application_Service / Component Rel_Realization Motivation_Requirement
|
||||||
|
Motivation_Driver Rel_Association Motivation_Stakeholder
|
||||||
|
Motivation_Assessment Rel_Association Motivation_Driver
|
||||||
|
Motivation_Goal Rel_Realization Motivation_Outcome
|
||||||
|
|
||||||
|
# Внедрение и миграция
|
||||||
|
Implementation_WorkPackage Rel_Realization Implementation_Deliverable
|
||||||
|
Implementation_Plateau Rel_Aggregation элементы core-слоёв
|
||||||
|
Implementation_Gap Rel_Association между двумя plateau
|
||||||
|
```
|
||||||
|
|
||||||
|
### Запреты
|
||||||
|
|
||||||
|
1. Не `Rel_Assignment` от Data Object или Business Object к процессу.
|
||||||
|
2. Не `Rel_Realization` от Component сразу к Process: нужен внутренний Function или Process, затем Realization к Service.
|
||||||
|
3. Не `Rel_Serving` в направлении «потребитель → сервис».
|
||||||
|
4. Не подменять Access потоком Flow только потому, что речь о данных.
|
||||||
|
5. Не ставить `Business_Process` там, где исполнитель — приложение.
|
||||||
|
6. Не ставить Association на все рёбра «на всякий случай».
|
||||||
|
7. Не считать Device подтипом Node без композиции или агрегации.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Viewpoints
|
||||||
|
|
||||||
|
**Один рисунок = один viewpoint.** Не сваливать мотивацию, бизнес, приложения и инфраструктуру на один холст (Layered — исключение, но и там ограничивай число элементов).
|
||||||
|
|
||||||
|
| Viewpoint | Фокус | Типичные элементы |
|
||||||
|
|-----------|-------|-------------------|
|
||||||
|
| Organization | структура организационных единиц | Actor, Role, Collaboration, Location |
|
||||||
|
| Application Structure | внутренняя структура приложений | Component, Interface, Data Object |
|
||||||
|
| Information Structure | данные и информация | Business Object, Representation, Data Object, Artifact |
|
||||||
|
| Technology | инфраструктура | Node, Device, System Software, Network, Path |
|
||||||
|
| Layered | обзор слоёв | Business + Application + Technology, Serving и Realization |
|
||||||
|
| Physical | физический мир | Equipment, Facility, Distribution Network, Material |
|
||||||
|
| Product | ценность продукта | Product, Contract, Service, Interface |
|
||||||
|
| Application Usage | приложения в бизнес-контексте | бизнес-процессы + прикладные сервисы |
|
||||||
|
| Technology Usage | технологии для приложений | Application + Technology services и nodes |
|
||||||
|
| Business Process Cooperation | связь процессов | Process, Event, Service, Role |
|
||||||
|
| Application Cooperation | связь приложений | Component, Service, Interface, Data, Flow |
|
||||||
|
| Service Realization | как реализованы сервисы | Service ← Process / Function ← активная структура |
|
||||||
|
| Implementation and Deployment | развёртывание | Application + Artifact + Node / Device |
|
||||||
|
| Stakeholder, Goal Realization, Requirements Realization, Motivation | мотивация | Driver, Goal, Outcome, Requirement, Constraint |
|
||||||
|
| Strategy, Capability Map, Value Stream, Resource Map | стратегия | Capability, Resource, Value Stream, Course of Action |
|
||||||
|
| Project, Migration, Implementation and Migration | внедрение | Work Package, Deliverable, Plateau, Gap |
|
||||||
|
|
||||||
|
### Макет
|
||||||
|
|
||||||
|
- Структурные схемы: `LAYOUT_TOP_DOWN()` или вложенность через `{ }`.
|
||||||
|
- Процессы слева направо: `LAYOUT_LEFT_RIGHT()` и `Rel_*_Right`.
|
||||||
|
- Layered: бизнес сверху, технологии снизу; Serving и Realization направлены вверх (`Rel_Serving_Up`).
|
||||||
|
- Выравнивание скрытыми связями: `Lay_U`, `Lay_D`, `Lay_L`, `Lay_R`.
|
||||||
|
- Вложение элементов:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Business_Product(prod, "Тариф") {
|
||||||
|
Business_Service(s1, "Подключение")
|
||||||
|
Business_Contract(c1, "Оферта")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Для Service, Actor, Value и ValueStream при вложении добавляется `$nest=%true()`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Готовые шаблоны
|
||||||
|
|
||||||
|
Тела для ` ```uml-archimate ` без обёртки.
|
||||||
|
|
||||||
|
### Organization
|
||||||
|
|
||||||
|
```text
|
||||||
|
LAYOUT_TOP_DOWN()
|
||||||
|
Business_Actor(uni, "Университет") {
|
||||||
|
Business_Actor(dean, "Деканат")
|
||||||
|
Business_Actor(it, "Управление ИТ")
|
||||||
|
}
|
||||||
|
Business_Role(student, "Студент")
|
||||||
|
Business_Role(clerk, "Методист")
|
||||||
|
Rel_Assignment(dean, clerk)
|
||||||
|
Rel_Assignment(uni, student)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Service Realization
|
||||||
|
|
||||||
|
```text
|
||||||
|
LAYOUT_TOP_DOWN()
|
||||||
|
Business_Role(applicant, "Абитуриент")
|
||||||
|
Business_Process(apply, "Подача заявления")
|
||||||
|
Business_Service(applySvc, "Приём заявлений")
|
||||||
|
Application_Service(webSvc, "Веб-приёмная")
|
||||||
|
Application_Component(lk, "Личный кабинет")
|
||||||
|
Application_Function(submitFn, "Отправка формы")
|
||||||
|
|
||||||
|
Rel_Assignment(applicant, apply)
|
||||||
|
Rel_Realization(apply, applySvc)
|
||||||
|
Rel_Serving(webSvc, apply)
|
||||||
|
Rel_Assignment(lk, submitFn)
|
||||||
|
Rel_Realization(submitFn, webSvc)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Application Cooperation
|
||||||
|
|
||||||
|
```text
|
||||||
|
LAYOUT_LEFT_RIGHT()
|
||||||
|
Application_Component(crm, "CRM")
|
||||||
|
Application_Component(billing, "Биллинг")
|
||||||
|
Application_Service(crmApi, "API клиентов")
|
||||||
|
Application_DataObject(client, "Карточка клиента")
|
||||||
|
|
||||||
|
Rel_Realization(crm, crmApi)
|
||||||
|
Rel_Serving(crmApi, billing)
|
||||||
|
Rel_Access_rw(crm, client)
|
||||||
|
Rel_Access_r(billing, client)
|
||||||
|
Rel_Flow(crm, billing, "событие оплаты")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Layered
|
||||||
|
|
||||||
|
```text
|
||||||
|
LAYOUT_TOP_DOWN()
|
||||||
|
Business_Process(sale, "Оформление заказа")
|
||||||
|
Business_Object(order, "Заказ")
|
||||||
|
Application_Service(shopSvc, "Интернет-магазин")
|
||||||
|
Application_Component(shop, "ShopApp")
|
||||||
|
Technology_Service(dbSvc, "СУБД")
|
||||||
|
Technology_Node(dbHost, "Сервер БД")
|
||||||
|
Technology_SystemSoftware(pg, "PostgreSQL")
|
||||||
|
|
||||||
|
Rel_Access_w(sale, order)
|
||||||
|
Rel_Serving_Up(shopSvc, sale)
|
||||||
|
Rel_Realization(shop, shopSvc)
|
||||||
|
Rel_Serving_Up(dbSvc, shop)
|
||||||
|
Rel_Assignment(dbHost, dbSvc)
|
||||||
|
Rel_Composition(dbHost, pg)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Requirements Realization
|
||||||
|
|
||||||
|
```text
|
||||||
|
LAYOUT_TOP_DOWN()
|
||||||
|
Motivation_Stakeholder(rector, "Ректор")
|
||||||
|
Motivation_Driver(comp, "Конкуренция вузов")
|
||||||
|
Motivation_Goal(digital, "Цифровизация приёма")
|
||||||
|
Motivation_Requirement(online, "Заявление онлайн круглосуточно")
|
||||||
|
Motivation_Constraint(fz152, "152-ФЗ")
|
||||||
|
Application_Service(portal, "Портал абитуриента")
|
||||||
|
|
||||||
|
Rel_Association(rector, comp)
|
||||||
|
Rel_Association(comp, digital)
|
||||||
|
Rel_Realization(online, digital)
|
||||||
|
Rel_Influence(fz152, online, "-")
|
||||||
|
Rel_Realization(portal, online)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Implementation & Migration
|
||||||
|
|
||||||
|
```text
|
||||||
|
LAYOUT_LEFT_RIGHT()
|
||||||
|
Implementation_Plateau(asIs, "AS-IS 2025")
|
||||||
|
Implementation_Plateau(toBe, "TO-BE 2026")
|
||||||
|
Implementation_Gap(gap1, "Нет единого личного кабинета")
|
||||||
|
Implementation_WorkPackage(wp, "Проект «Единый ЛК»")
|
||||||
|
Implementation_Deliverable(d1, "Релиз 1.0")
|
||||||
|
Implementation_Event(goLive, "Ввод в эксплуатацию")
|
||||||
|
|
||||||
|
Rel_Association(gap1, asIs)
|
||||||
|
Rel_Association(gap1, toBe)
|
||||||
|
Rel_Realization(wp, d1)
|
||||||
|
Rel_Triggering(d1, goLive)
|
||||||
|
Rel_Triggering(goLive, toBe)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Technology Usage
|
||||||
|
|
||||||
|
```text
|
||||||
|
LAYOUT_TOP_DOWN()
|
||||||
|
Application_Component(api, "API Gateway")
|
||||||
|
Technology_Node(k8s, "Кластер Kubernetes")
|
||||||
|
Technology_Device(node1, "Рабочий узел")
|
||||||
|
Technology_SystemSoftware(os, "Linux")
|
||||||
|
Technology_CommunicationNetwork(net, "Сеть кластера")
|
||||||
|
Technology_Artifact(image, "api:1.2")
|
||||||
|
|
||||||
|
Rel_Serving_Up(k8s, api)
|
||||||
|
Rel_Composition(k8s, node1)
|
||||||
|
Rel_Composition(node1, os)
|
||||||
|
Rel_Association(node1, net)
|
||||||
|
Rel_Assignment(node1, image)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Процесс с Junction
|
||||||
|
|
||||||
|
```text
|
||||||
|
LAYOUT_LEFT_RIGHT()
|
||||||
|
Business_Event(incident, "Инцидент")
|
||||||
|
Business_Process(triage, "Первичная оценка")
|
||||||
|
Junction_Or(j1, "")
|
||||||
|
Business_Process(restore, "Восстановление сервиса")
|
||||||
|
Business_Process(investigate, "Расследование")
|
||||||
|
Business_Process(close, "Закрытие инцидента")
|
||||||
|
|
||||||
|
Rel_Triggering(incident, triage)
|
||||||
|
Rel_Triggering(triage, j1)
|
||||||
|
Rel_Triggering(j1, restore)
|
||||||
|
Rel_Triggering(j1, investigate)
|
||||||
|
Rel_Triggering(restore, close)
|
||||||
|
Rel_Triggering(investigate, close)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Антипаттерны
|
||||||
|
|
||||||
|
| Плохо | Почему | Как надо |
|
||||||
|
|-------|--------|----------|
|
||||||
|
| все связи `Rel_Association` | теряется семантика | Serving, Realization, Assignment, Access |
|
||||||
|
| `Application_Component` вместо `Business_Actor` | разные аспекты и слои | Actor и Role для людей и подразделений |
|
||||||
|
| Business Process «внутри» компонента без Assignment | слои смешаны | процесс на бизнес-слое, Serving от прикладного сервиса |
|
||||||
|
| Serving от Role к Service | направление наоборот | Service → Role или Process |
|
||||||
|
| Realization Component → Process | пропущено внутреннее поведение | Component → Function → Service |
|
||||||
|
| Flow вместо Access к Data Object | Flow — передача, Access — работа с объектом | `Rel_Access_r/w/rw` |
|
||||||
|
| один Layered на 40 элементов | нечитаемо | несколько viewpoint'ов |
|
||||||
|
| C4 `Person` или UML-класс в блоке archimate | другой язык | только макросы ArchiMate |
|
||||||
|
| `Process` без указания слоя | в 3.2 слой обязателен | `Business_Process`, `Application_Process`, `Technology_Process` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Чеклист генерации
|
||||||
|
|
||||||
|
1. Версия языка 3.2, макросы с указанием слоя.
|
||||||
|
2. Оградка ` ```uml-archimate `, без `@startuml` и `!include`.
|
||||||
|
3. Выбран **один** viewpoint; 8–18 элементов на рисунок.
|
||||||
|
4. Сначала объявлены все элементы, затем все `Rel_*`.
|
||||||
|
5. Alias латиницей, подписи по-русски в кавычках.
|
||||||
|
6. Serving — от сервиса к потребителю; Realization — от конкретного к абстрактному.
|
||||||
|
7. Поведение того же слоя, что и его исполнитель по Assignment.
|
||||||
|
8. Нет C4, UML-классов, BPMN-развилок и Association «по умолчанию».
|
||||||
|
9. Перед блоком стоит `%id Название`, при широкой схеме — `+landscape`.
|
||||||
|
10. В тексте есть ссылка `@Рисунок:id`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Запасной вариант без stdlib
|
||||||
|
|
||||||
|
Если библиотека ArchiMate недоступна, схему можно нарисовать в ` ```uml ` низкоуровневым ключевым словом:
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
%arch_fallback Фрагмент архитектуры
|
||||||
|
|
||||||
|
```uml
|
||||||
|
@startuml
|
||||||
|
archimate #Business "Подача заявки" as p <<business-process>>
|
||||||
|
archimate #Application "Портал" as c <<application-component>>
|
||||||
|
c --> p
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Смешивать макросы и низкоуровневый `archimate` на одной схеме нельзя.
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
# Проверка ТЗ — полный каталог замечаний
|
||||||
|
|
||||||
|
Что именно проверяет `md2gost … --check`, с какими текстами сообщений и как чинить каждое замечание.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Как запускать
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m md2gost report.md --check-only --type coursework # только проверка
|
||||||
|
python -m md2gost report.md -o out.docx --type coursework --check --strict
|
||||||
|
```
|
||||||
|
|
||||||
|
| Ключ | Поведение |
|
||||||
|
|------|-----------|
|
||||||
|
| `--check` | проверить и напечатать отчёт, документ всё равно собрать |
|
||||||
|
| `--check-only` | только проверка, DOCX не собирается, код выхода 0 |
|
||||||
|
| `--strict` | при наличии хотя бы одной ошибки вывести `Проверка ТЗ: есть ошибки (--strict)`, код выхода 1, документ не собирать |
|
||||||
|
| `--check-pages` | отдельная эвристическая проверка вёрстки готового DOCX через Microsoft Word |
|
||||||
|
|
||||||
|
Формат отчёта:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Проверка ТЗ: найдено замечаний — 3
|
||||||
|
[error] structure.missing: Отсутствует обязательный раздел «ЗАКЛЮЧЕНИЕ»
|
||||||
|
[warning] ref.unused (стр. md:42): Метка «tab_cmp» объявлена, но в тексте нет ссылки @…:tab_cmp
|
||||||
|
[error] ref.ris (стр. md:57): Используйте «Рисунок», не «рис.»
|
||||||
|
```
|
||||||
|
|
||||||
|
Без замечаний: `Проверка ТЗ: замечаний нет.`
|
||||||
|
|
||||||
|
Уровни: **error** — блокирует сборку при `--strict`; **warning** — не блокирует; **heuristic** — только у проверки вёрстки, никогда не влияет на код выхода.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Структура документа
|
||||||
|
|
||||||
|
| ID | Уровень | Сообщение | Причина и решение |
|
||||||
|
|----|---------|-----------|-------------------|
|
||||||
|
| `structure.missing` | error | `Отсутствует обязательный раздел «{название}»` | Нет одного из: СОДЕРЖАНИЕ, ВВЕДЕНИЕ, ЗАКЛЮЧЕНИЕ, СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ. Добавить раздел `# *НАЗВАНИЕ` с содержательным текстом. Допускается написание «СПИСОК ИСПОЛЬЗУЕМЫХ ИСТОЧНИКОВ» |
|
||||||
|
| `structure.missing` | error | `Отсутствует раздел «СОДЕРЖАНИЕ»` | Профиль `PIS_custom`: нет `# *СОДЕРЖАНИЕ` |
|
||||||
|
| `structure.toc` | warning | `Нет маркера [TOC] для автособираемого содержания` | Добавить `[TOC]` прописными отдельной строкой под `# *СОДЕРЖАНИЕ` |
|
||||||
|
| `structure.toc` | **error** | `Нет маркера [TOC] для оглавления (обязательно для итогового отчёта)` | То же, но для `PIS_custom` это ошибка |
|
||||||
|
| `structure.body` | error | `Нет нумерованного раздела основной части (# заголовок без *)` | Все H1 оказались со звёздочкой. Добавить `# 1 Название` |
|
||||||
|
| `structure.graphic` | error | `Для ВКР обязательно приложение «Графический материал»` | В тексте нет подстроки «Графический материал». Добавить `## Приложение Б Графический материал` |
|
||||||
|
| `structure.practical` | error | `Нет заголовков вида «Практическая работа №N. …» (H1 без *, каждая работа — отдельный раздел)` | Профиль `PIS_custom`: каждую работу оформить как `# Практическая работа №N. Название` |
|
||||||
|
| `structure.apid` | error | `Для КР АПИД отсутствует обязательный раздел: «{название}»` | См. таблицу ниже |
|
||||||
|
|
||||||
|
### Обязательные разделы КР АПИД
|
||||||
|
|
||||||
|
Проверка ищет подстроки в тексте заголовков (регистр не важен):
|
||||||
|
|
||||||
|
| Требуемый раздел | Что должно найтись в заголовках |
|
||||||
|
|------------------|--------------------------------|
|
||||||
|
| `1. Теоретические аспекты разработки архитектуры приложений и данных: …` | «теоретическ… аспект» |
|
||||||
|
| `2. Прикладные аспекты разработки архитектуры приложений и данных …` | «прикладн… аспект» |
|
||||||
|
| `2.1 Описание проекта команды и разрабатываемого программного приложения` | «2.1» и «описание проекта» рядом |
|
||||||
|
| `2.2 Описание роли … в приложении …` | «2.2» и «описание роли» рядом |
|
||||||
|
| `2.3 Описание архитектуры программного приложения и данных для роли` | «2.3» и «архитектур» |
|
||||||
|
| `2.4 Варианты развития архитектуры программного приложения` | «2.4» и «вариант» |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Заголовки
|
||||||
|
|
||||||
|
| ID | Уровень | Сообщение | Причина и решение |
|
||||||
|
|----|---------|-----------|-------------------|
|
||||||
|
| `heading.numbered_special` | error | `Спецраздел «{title}» должен быть без номера: # *{title}` | H1 со спецназванием без звёздочки. Добавить `*` сразу после `# ` |
|
||||||
|
| `heading.case` | warning | `Спецраздел лучше писать ПРОПИСНЫМИ: «{TITLE}»` | «Введение» вместо «ВВЕДЕНИЕ» |
|
||||||
|
| `heading.case` | warning | `Подраздел с прописной буквы: «{Title}»` | Профиль `PIS_custom`: заголовок уровня `##` и глубже начинается со строчной буквы |
|
||||||
|
| `heading.trailing_dot` | warning | `Заголовок без точки в конце: «{title}»` | Профиль `PIS_custom`: убрать точку в конце заголовка |
|
||||||
|
|
||||||
|
Спецразделы, требующие звёздочки на уровне H1: СОДЕРЖАНИЕ, ВВЕДЕНИЕ, ЗАКЛЮЧЕНИЕ, СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ, СПИСОК ИСПОЛЬЗУЕМЫХ ИСТОЧНИКОВ, ПРИЛОЖЕНИЕ, ПРИЛОЖЕНИЯ, а также любой H1, начинающийся на «ПРИЛОЖЕН».
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Ссылки и терминология
|
||||||
|
|
||||||
|
| ID | Уровень | Сообщение | Причина и решение |
|
||||||
|
|----|---------|-----------|-------------------|
|
||||||
|
| `ref.ris` | error | `Используйте «Рисунок», не «рис.»` | В тексте найдено «рис.» (шаблон `\bрис\.`). Заменить на `@Рисунок:id` или слово «Рисунок» |
|
||||||
|
| `ref.tabl` | error | `Используйте «Таблица», не «табл.»` | Найдено «табл.». Заменить на `@Таблица:id` или слово «Таблица» |
|
||||||
|
| `footnote` | error | `Сноски в конце страницы не допускаются` | Найдена конструкция `[^…]`. Перенести содержание в основной текст или оформить ссылкой на источник |
|
||||||
|
| `table.nopp` | error | `Графу «№ п/п» в таблицы не включают` | Найдено «№ п/п». Удалить столбец |
|
||||||
|
| `ref.unused` | warning | `Метка «{id}» объявлена, но в тексте нет ссылки @…:{id}` | Добавить в текст перед объектом предложение со ссылкой `@Тип:{id}`, заканчивающееся точкой |
|
||||||
|
| `ref.dangling` | error | `Ссылка на несуществующую метку «{id}»` | Опечатка в `id` либо отсутствие объекта с такой меткой |
|
||||||
|
|
||||||
|
Множество меток собирается из строк `^%(\w+)` и из title картинок; множество ссылок — из `@Тип:id`. Оба множества должны совпадать.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Источники
|
||||||
|
|
||||||
|
| ID | Уровень | Сообщение | Причина и решение |
|
||||||
|
|----|---------|-----------|-------------------|
|
||||||
|
| `cite.intro` | error | `Во разделе «ВВЕДЕНИЕ» ссылки на источники не указываются` | Убрать `[n]` из введения; то же для заключения |
|
||||||
|
| `biblio.count` | error | `Источников должно быть от {min} до {max} (сейчас {n})` | Довести число записей до нормы профиля |
|
||||||
|
| `biblio.count` | warning | `ВКР: в разделе списка «{title}» желательно ≥10 источников (сейчас {n})` | Дополнить подраздел списка |
|
||||||
|
| `biblio.count` | warning | `ВКР: список должен делиться на разделы; в каждом ≥10 источников (сейчас всего {n}, разделов нет)` | Разбить список подразделами `##` |
|
||||||
|
| `biblio.age` | error | `Источник [{n}] старше {N} лет (год {год})` | Заменить на актуальное издание или редакцию стандарта |
|
||||||
|
| `biblio.order` | warning | `Порядок источников в списке должен совпадать с порядком первого упоминания` | Перенумеровать согласованно и в тексте, и в списке |
|
||||||
|
| `biblio.missing` | error | `Ссылка [{n}] есть в тексте, но нет в списке источников` | Добавить запись `[n]: …` |
|
||||||
|
|
||||||
|
Нормы по профилям:
|
||||||
|
|
||||||
|
| Профиль | Источников | Возраст |
|
||||||
|
|---------|-----------|---------|
|
||||||
|
| `practice`, `coursework` | 5–7 | не старше 5 лет |
|
||||||
|
| `APID_coursework` | 7–20 | не старше 5 лет |
|
||||||
|
| `vkr` | не менее 10 в каждом подразделе списка | не старше 5 лет |
|
||||||
|
| `PIS_custom` | не требуются | не старше 10 лет, если список всё же есть |
|
||||||
|
|
||||||
|
Год издания извлекается из текста записи автоматически. Если года в записи нет, проверка возраста пропускается.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Таблицы
|
||||||
|
|
||||||
|
| ID | Уровень | Сообщение | Причина и решение |
|
||||||
|
|----|---------|-----------|-------------------|
|
||||||
|
| `table.merge_header` | error | `Маркер склеивания ^/> нельзя использовать в первой (заголовочной) строке таблицы` | Вписать нормальный заголовок столбца |
|
||||||
|
| `table.merge_col` | error | `Маркер «>» нельзя ставить в первом столбце (нет ячейки слева)` | Убрать маркер |
|
||||||
|
| `table.continuation` | warning | `Word сам не вставит «Продолжение Таблицы N» при переносе…` | Разметка ни при чём: запускать с `--table-continuation word` (нужен Word и `pywin32`) либо `caption` |
|
||||||
|
| `listing.continuation` | warning | `Длинный листинг: Word сам не вставит «Продолжение Листинга N»…` | То же для `--listing-continuation`. Длинным считается блок кода от 40 строк — можно вынести полный код в приложение |
|
||||||
|
|
||||||
|
Замечания о продолжении выдаются только при режимах `off` и `soft`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Приложения
|
||||||
|
|
||||||
|
| ID | Уровень | Сообщение | Причина и решение |
|
||||||
|
|----|---------|-----------|-------------------|
|
||||||
|
| `appendix.letter` | error | `Буква «{X}» не используется для обозначения приложений` | Запрещены Ё, З, Й, О, Ч, Ь, Ы, Ъ. Допустимы А, Б, В, Г, Д, Е, Ж, И, К, Л, М, Н, П, Р, С, Т, У, Ф, Х, Ц, Ш, Щ, Э, Ю, Я |
|
||||||
|
| `appendix.toc` | warning | `При нескольких приложениях после «ПРИЛОЖЕНИЯ» нужен перечень (основной текст) с номерами и названиями` | Добавить строки вида `Приложение А — Название` сразу после `# *ПРИЛОЖЕНИЯ` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Проверка вёрстки `--check-pages`
|
||||||
|
|
||||||
|
Отдельная эвристика на готовом DOCX: ищет страницы, у которых снизу пусто больше 40 % полосы, а раздел продолжается на следующей странице.
|
||||||
|
|
||||||
|
| Ситуация | Сообщение |
|
||||||
|
|----------|-----------|
|
||||||
|
| Нет `pywin32` или Word | `Проверка вёрстки пропущена: нужен пакет pywin32 (pip install pywin32) и Microsoft Word.` |
|
||||||
|
| Word не открылся | `Проверка вёрстки пропущена: не удалось открыть Word ({ошибка}).` |
|
||||||
|
| Замечаний нет | `Проверка вёрстки (Word): замечаний по полупустым страницам нет (эвристика; возможны пропуски).` |
|
||||||
|
| Замечания есть | `Проверка вёрстки (Word): найдено замечаний — {N} (все эвристические, могут быть ложными).` |
|
||||||
|
| Конкретная страница | `[heuristic] page.fill: на стр. {N} пустой низ ~{X}% полосы, а раздел «{название}» продолжается на следующей странице…` |
|
||||||
|
|
||||||
|
Ложные срабатывания нормальны: крупный рисунок, конец пункта, намеренный разрыв. Чем помочь на уровне разметки: флаг `+landscape` у широкой схемы, перенос крупного объекта в приложение, сокращение листинга.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Рекомендуемый цикл доводки
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m md2gost report.md --check-only --type coursework # 1. посмотреть замечания
|
||||||
|
# 2. отдать текст замечаний и .md модели с промптом ai/prompts/review-fix.md
|
||||||
|
python -m md2gost report.md --check-only --type coursework # 3. проверить повторно
|
||||||
|
python -m md2gost report.md -o report.docx --type coursework --check --strict
|
||||||
|
python -m md2gost report.docx --check-pages # 4. проверить вёрстку
|
||||||
|
```
|
||||||
|
|
||||||
|
Что проверка **не** ловит: смысловые ошибки, фактические неточности, выдуманные источники, слабую логику разделов и «машинный» стиль изложения. Стиль лечится промптом `ai/prompts/voice-human.md`, факты — вычиткой.
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
# Запуск, профили и настройки md2gost
|
||||||
|
|
||||||
|
Справочник по CLI, типам документов, конфигурационным файлам и оформлению. Движок один: интерфейс собирает те же параметры, что и аргументы командной строки.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Основные команды
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m md2gost # окно интерфейса
|
||||||
|
python -m md2gost report.md -o report.docx --type coursework --check
|
||||||
|
python -m md2gost report.md --check-only --type vkr --strict
|
||||||
|
python -m md2gost report.docx -o report.md # импорт Word → Markdown
|
||||||
|
python -m md2gost report.docx --check-pages # проверка вёрстки готового DOCX
|
||||||
|
python -m word2md report.docx -o report.md # то же импортом напрямую
|
||||||
|
python -m md2fodt report.md -o report.fodt # экспорт в формат LibreOffice
|
||||||
|
md2gost-gui # окно интерфейса, точка входа
|
||||||
|
```
|
||||||
|
|
||||||
|
Без имени файла или с ключом `--gui` открывается окно. Windows-сборка: `build-exe.bat` даёт `dist\md2gost.exe`, двойной клик открывает окно, аргументы работают так же.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Типы документов `--type`
|
||||||
|
|
||||||
|
| Тип | Назначение | Нумерация объектов | Источники | Стиль |
|
||||||
|
|-----|-----------|--------------------|-----------|-------|
|
||||||
|
| `practice` (по умолчанию) | отчёт по практике | по разделам: 1.1, 2.3 | 5–7, не старше 5 лет | МИРЭА |
|
||||||
|
| `coursework` | курсовая работа | по разделам | 5–7, не старше 5 лет | МИРЭА |
|
||||||
|
| `vkr` | выпускная квалификационная работа | по разделам | не менее 10 в каждом подразделе списка | МИРЭА |
|
||||||
|
| `APID_coursework` | курсовая «Архитектура приложений и данных» | по разделам | 7–20, не старше 5 лет | МИРЭА |
|
||||||
|
| `PIS_custom` | итоговый отчёт по практическим работам | сквозная: 1, 2, 3 | не требуются | ПИС |
|
||||||
|
|
||||||
|
Что меняет профиль:
|
||||||
|
|
||||||
|
| Свойство | МИРЭА-профили | `PIS_custom` |
|
||||||
|
|----------|---------------|--------------|
|
||||||
|
| H1 | 18 пт, ПРОПИСНЫЕ, слева (СОДЕРЖАНИЕ и СПИСОК — по центру) | 14 пт, ПРОПИСНЫЕ, по центру |
|
||||||
|
| H2 / H3 | 16 пт / 14 пт | 14 пт, по ширине, отступ 1,25 см |
|
||||||
|
| Введение и заключение | обязательны | не требуются |
|
||||||
|
| Список источников | обязателен | не требуется |
|
||||||
|
| Особая структура | у `vkr` — приложение «Графический материал»; у `APID_coursework` — фиксированные заголовки разделов | H1 вида «Практическая работа №N. …» |
|
||||||
|
|
||||||
|
Общее оформление обоих пресетов: Times New Roman 14, интервал 1,5, поля 30/10/20/20 мм, абзацный отступ 1,25 см, выравнивание по ширине, подпись рисунка под ним, надписи таблицы и листинга сверху.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Все ключи командной строки
|
||||||
|
|
||||||
|
### Ввод и вывод
|
||||||
|
|
||||||
|
| Ключ | По умолчанию | Смысл |
|
||||||
|
|------|--------------|-------|
|
||||||
|
| `filename` | — | путь к `.md` (сборка DOCX) или `.docx` (импорт в Markdown) |
|
||||||
|
| `-o`, `--output` | рядом с исходником | путь результата |
|
||||||
|
| `-t`, `--template` | встроенный `Template.docx` | свой шаблон DOCX |
|
||||||
|
| `--gui` | — | открыть окно, можно сразу с файлом |
|
||||||
|
| `--type` | `practice` | тип документа |
|
||||||
|
|
||||||
|
### Структура и разметка
|
||||||
|
|
||||||
|
| Ключ | По умолчанию | Смысл |
|
||||||
|
|------|--------------|-------|
|
||||||
|
| `--heading-numbering {auto,manual}` | `manual` | `manual` — цифры из markdown, автонумерация Word отключена; `auto` — нумерует Word, цифры из заголовков снимаются |
|
||||||
|
| `--toc {manual,native}` | `native` | `native` — поле Word TOC (обновить при открытии); `manual` — md2gost собирает содержание с номерами страниц |
|
||||||
|
| `--emdash-to-hyphen` | выкл. | заменить все «—» на «-» |
|
||||||
|
| `--hr-pagebreak` | выкл. | строки `---`, `***`, `___` дают разрыв страницы (иначе игнорируются) |
|
||||||
|
| `--page-start N` | сквозная нумерация | начальный номер страницы в колонтитуле тела |
|
||||||
|
|
||||||
|
### Титул, задание, метаданные
|
||||||
|
|
||||||
|
| Ключ | По умолчанию | Смысл |
|
||||||
|
|------|--------------|-------|
|
||||||
|
| `--title` | — | готовый DOCX титульного листа; **отключает автогенерацию** |
|
||||||
|
| `--auto-title` | выкл. | собрать титул из `info_conv.yaml` и блока ` ```title ` |
|
||||||
|
| `--assignment` | — | DOCX бланка задания |
|
||||||
|
| `--student`, `--group` | из `md2gost.user.json` | ФИО и группа для автотитула |
|
||||||
|
| `--doc-author-from {os,student,custom}` | `os` | источник автора в свойствах DOCX |
|
||||||
|
| `--doc-author`, `--doc-last-modified-by`, `--doc-title`, `--doc-subject`, `--doc-keywords`, `--doc-comments`, `--doc-category` | — | поля свойств DOCX; пустое `--doc-comments` убирает штамп md2gost |
|
||||||
|
|
||||||
|
### Проверка
|
||||||
|
|
||||||
|
| Ключ | Смысл |
|
||||||
|
|------|-------|
|
||||||
|
| `--check` | проверить ТЗ и напечатать отчёт |
|
||||||
|
| `--check-only` | только проверка, без сборки |
|
||||||
|
| `--strict` | код выхода 1 при ошибках, документ не собирается |
|
||||||
|
| `--check-pages` | эвристическая проверка полупустых страниц через Microsoft Word (нужны Windows, Word, `pywin32`) |
|
||||||
|
|
||||||
|
Полный каталог замечаний — в `checker.md`.
|
||||||
|
|
||||||
|
### Таблицы и листинги через границу страницы
|
||||||
|
|
||||||
|
| Ключ | По умолчанию | Значения |
|
||||||
|
|------|--------------|----------|
|
||||||
|
| `--table-continuation` | `word` | `word` — после сохранения Word COM режет по реальной пагинации и подписывает «Продолжение Таблицы N»; `off` и `soft` — одна таблица без автоподписи; `legacy` и `caption` — резать по оценке высоты (возможны артефакты) |
|
||||||
|
| `--listing-continuation` | `word` | те же значения для листингов |
|
||||||
|
| `--table-repeat-header` | выкл. | повторять шапку таблицы на каждом фрагменте после разрыва |
|
||||||
|
|
||||||
|
Режим `word` требует Windows, установленный Microsoft Word и пакет `pywin32`. Без Word — `--table-continuation off`.
|
||||||
|
|
||||||
|
### Диаграммы
|
||||||
|
|
||||||
|
| Ключ | По умолчанию | Смысл |
|
||||||
|
|------|--------------|-------|
|
||||||
|
| `--plantuml-jar` | вшитый, иначе `PLANTUML_JAR` | путь к `plantuml.jar` |
|
||||||
|
| `--kroki-url` | `KROKI_URL`, иначе `http://localhost:8000` | локальный Kroki |
|
||||||
|
| `--diagram-fallback {local,remote,off}` | `remote` | что делать при недоступности локального рендера: `remote` — обратиться к `kroki.io`; `local` — ошибка; `off` — не рисовать |
|
||||||
|
| `--diagram-format {png,svg}` | `png` | формат вставки; `svg` даёт вектор с PNG-запасным (Word 2016+) |
|
||||||
|
| `--diagram-scale` | `2` | масштаб растра PlantUML и локального Mermaid; размер на странице как при 1 |
|
||||||
|
| `--schemes` | рядом с приложением или с `.md` | путь к `md2gost.schemes.json` |
|
||||||
|
| `--install-chromium` | — | скачать headless Chromium для Mermaid и выйти |
|
||||||
|
|
||||||
|
### Прочее
|
||||||
|
|
||||||
|
| Ключ | По умолчанию | Смысл |
|
||||||
|
|------|--------------|-------|
|
||||||
|
| `--styles` | пресет типа | путь к `md2gost.styles.json` |
|
||||||
|
| `--syntax-highlighting` | выкл. | подсветка синтаксиса в листингах |
|
||||||
|
| `--debug` | выкл. | отладочные данные в документе |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Импорт Word → Markdown
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m word2md report.docx -o report.md
|
||||||
|
```
|
||||||
|
|
||||||
|
| Ключ | Смысл |
|
||||||
|
|------|-------|
|
||||||
|
| `-o`, `--output` | путь к `.md`, по умолчанию рядом с DOCX |
|
||||||
|
| `--media-dir` | каталог картинок, по умолчанию `<имя>_media` |
|
||||||
|
| `--keep-toc-pages` | сохранить строки содержания Word (иначе остаётся только `[TOC]`) |
|
||||||
|
| `--pagebreaks` | писать `---` на разрывах страниц |
|
||||||
|
|
||||||
|
Что распознаётся: заголовки и спецразделы, содержание, подписи «Рисунок N — …» / «Таблица N» / «Листинг N» (включая склейку «Продолжение…»), таблицы с объединением в `^` и `>`, листинги, формулы OMML в `$$…$$`, библиография `[n]: …`, ссылки `[n]`, часть перекрёстных ссылок восстанавливается как `@Рисунок:fig1_1`.
|
||||||
|
|
||||||
|
Что теряется: исходники PlantUML и Mermaid (остаются только картинки), язык оградки листингов, осмысленные `id` меток, надписи и SmartArt, сноски, плавающие рисунки. Титул и бланк задания до первого заголовка пропускаются, колонтитулы не импортируются.
|
||||||
|
|
||||||
|
После импорта результат имеет смысл довести промптом `ai/prompts/from-word.md` и проверить `--check`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Экспорт в FODT
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m md2fodt report.md -o report.fodt
|
||||||
|
```
|
||||||
|
|
||||||
|
Ключ `--emdash-to-hyphen` работает так же. PDF через LaTeX — каталог `md2latex/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Конфигурационные файлы
|
||||||
|
|
||||||
|
| Файл | Где лежит | Назначение |
|
||||||
|
|------|-----------|------------|
|
||||||
|
| `md2gost.user.json` | рядом с приложением | ФИО, группа, блок `metadata` со свойствами DOCX |
|
||||||
|
| `md2gost.schemes.json` | рядом с приложением, рядом с `.md`, либо путь в `--schemes` | схемы диаграмм: `title`, `docs`, `ai-prompt`, `includes`, `prefix`, `postfix`, `theme` |
|
||||||
|
| `md2gost.styles.json` | рядом с `.md` или путь в `--styles` | оверлей оформления поверх пресета |
|
||||||
|
| `info_conv.yaml` | рядом с `.md` | институт, кафедра, дисциплина, преподаватель, должность |
|
||||||
|
| `.md2gost-cache/` | рядом с `.md` | кэш отрисованных диаграмм |
|
||||||
|
| `md2gost.include-cache.json`, `include-cache/` | рядом с приложением | кэш скачанных `.puml` |
|
||||||
|
|
||||||
|
Приоритет загрузки, где каждый следующий слой сильнее: встроенный шаблон → файл рядом с приложением → файл рядом с `.md` → явный путь в ключе.
|
||||||
|
|
||||||
|
### `info_conv.yaml`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
Институт: Институт информационных технологий (ИИТ)
|
||||||
|
Кафедра: Кафедра цифровой трансформации (ЦТ)
|
||||||
|
Дисциплина: Проектирование архитектуры цифровой организации
|
||||||
|
Преподаватель: Иванов Иван Иванович
|
||||||
|
Должность: старший преподаватель, кафедра ЦТ
|
||||||
|
```
|
||||||
|
|
||||||
|
Плоский список `ключ: значение` в UTF-8. Строки с `#` — комментарии. ФИО и группа студента из файла **не читаются**: они берутся из профиля или ключей `--student` / `--group`.
|
||||||
|
|
||||||
|
### Автотитул: условия срабатывания
|
||||||
|
|
||||||
|
1. Включён `--auto-title` (или галочка «Автотитул»).
|
||||||
|
2. Не задан готовый `--title`.
|
||||||
|
3. Есть `info_conv.yaml` рядом с `.md` **или** блок ` ```title ` в начале файла.
|
||||||
|
4. Известны ФИО и группа — иначе конвертация останавливается с подсказкой «Для генерации титула нужны ФИО и группа студента».
|
||||||
|
|
||||||
|
Номер работы и год берутся из блока ` ```title ` (`number`, `year`); значения из блока сильнее `info_conv.yaml`. По умолчанию номер `1`, год — текущий.
|
||||||
|
|
||||||
|
### `md2gost.styles.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"page": { "left_mm": 30, "right_mm": 10, "top_mm": 20, "bottom_mm": 20 },
|
||||||
|
"styles": {
|
||||||
|
"Heading 1": { "alignment": "center", "size_pt": 14, "all_caps": true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Допустимые имена стилей: `Normal`, `Heading 1`, `Heading 2`, `Heading 3`, `Caption Figure`, `Caption Table`, `Название таблицы`, `Caption Listing`, `Caption`, `Code`, `Table Text`, `Bibliography`, `Bibliography Heading`, `toc 1`, `toc 2`, `toc 3`, `Footer`, `Hyperlink`, `FollowedHyperlink`, `Space After Table`.
|
||||||
|
|
||||||
|
Допустимые поля стиля: `font_name`, `size_pt`, `bold`, `italic`, `all_caps`, `underline`, `alignment` (`left` / `center` / `justify`), `first_line_indent_cm`, `left_indent_cm`, `right_indent_cm`, `space_before_mm`, `space_after_mm`, `line_spacing` (`1.0` / `1.5`), `page_break_before`, `keep_with_next`, `widow_control`.
|
||||||
|
|
||||||
|
Неизвестный ключ или неизвестное имя стиля — ошибка запуска. Не настраивается через JSON: табуляция оглавления, отступы списков, оформление формул, цвет текста (всегда чёрный).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Переменные окружения
|
||||||
|
|
||||||
|
| Переменная | Назначение |
|
||||||
|
|------------|------------|
|
||||||
|
| `PLANTUML_JAR` | путь к `plantuml.jar` |
|
||||||
|
| `KROKI_URL` | адрес локального Kroki, по умолчанию `http://localhost:8000` |
|
||||||
|
| `PLAYWRIGHT_BROWSERS_PATH` | каталог Chromium для Mermaid |
|
||||||
|
| `WORKING_DIR` | родительский каталог кэша диаграмм |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Где что в окне
|
||||||
|
|
||||||
|
| Меню | Содержимое |
|
||||||
|
|------|------------|
|
||||||
|
| Основные | тип документа, выходной путь, нумерация заголовков, содержание, режимы продолжения таблиц и листингов, смещение страниц |
|
||||||
|
| Настройки → Файлы | шаблон, титульный лист, бланк задания, стили JSON |
|
||||||
|
| Настройки → Студент | ФИО и группа (спрашиваются при первом запуске) |
|
||||||
|
| Настройки → Метаданные | свойства DOCX |
|
||||||
|
| Настройки → Диаграммы | PlantUML, Graphviz для DFD, Chromium для Mermaid, Kroki, формат, сброс кэша |
|
||||||
|
| Шаблоны UML | редактор `md2gost.schemes.json` |
|
||||||
|
| Справка | инструкция, документация, схемы, промпт для ИИ |
|
||||||
|
|
||||||
|
Кнопка «Импорт DOCX→MD» и перетаскивание `.docx` запускают импорт; перетаскивание `.md` подставляет файл для сборки.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Установка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -e .
|
||||||
|
pip install pywin32 # Windows: продолжение таблиц через Word COM
|
||||||
|
```
|
||||||
|
|
||||||
|
Нужен Python 3.10 или новее (для DFD — 3.11 и новее). Для PlantUML нужна Java; для Mermaid — Chrome, Edge или скачиваемый Chromium; для DFD — Graphviz. IDEF0 рисуется локально без внешних зависимостей.
|
||||||
@@ -0,0 +1,567 @@
|
|||||||
|
# Диаграммы в md2gost — справочник нотаций
|
||||||
|
|
||||||
|
Конвертер сам отрисовывает исходники диаграмм и вставляет их как Рисунок. Экспортировать схему в PNG вручную не нужно и вредно: картинка не пересобирается при правке.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Общие правила
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
Диаграмма прецедентов приведена на @Рисунок:uc_main.
|
||||||
|
|
||||||
|
%uc_main Диаграмма прецедентов
|
||||||
|
```
|
||||||
|
…и сразу после — fenced-блок с исходником.
|
||||||
|
|
||||||
|
| Элемент | Правило |
|
||||||
|
|---------|---------|
|
||||||
|
| Метка и подпись | строка `%id Подпись` **перед** блоком |
|
||||||
|
| Ссылка в тексте | `@Рисунок:id` в предложении **до** блока |
|
||||||
|
| `id` | латиница, цифры, `_`; начинается с буквы; без дефисов |
|
||||||
|
| `+listing` в подписи | дополнительно выводит исходник схемы как Листинг со своим номером |
|
||||||
|
| `+landscape` в подписи | размещает рисунок на альбомной странице |
|
||||||
|
|
||||||
|
Подпись не должна содержать самостоятельных слов `listing` и `landscape` — они будут вырезаны как флаги.
|
||||||
|
|
||||||
|
Внутри блоков диаграмм типографская автозамена **не** работает, поэтому двойные кавычки и дефисы в синтаксисе схем сохраняются как есть. Это единственные защищённые блоки: в блоках кода кавычки заменяются.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Таблица оградок
|
||||||
|
|
||||||
|
| Оградка | Тело | Движок |
|
||||||
|
|---------|------|--------|
|
||||||
|
| ` ```uml `, ` ```plantuml ` | полный PlantUML с `@startuml` … `@enduml` | PlantUML |
|
||||||
|
| ` ```uml-c4 `, ` ```c4 ` | макросы C4 Container, без обёртки | PlantUML + stdlib C4 |
|
||||||
|
| ` ```uml-c4context ` | макросы C4 System Context | PlantUML + stdlib C4 |
|
||||||
|
| ` ```uml-c4component ` | макросы C4 Component | PlantUML + stdlib C4 |
|
||||||
|
| ` ```usecase `, ` ```uml-usecase ` | `actor`, `usecase`, связи, без обёртки | PlantUML |
|
||||||
|
| ` ```archimate `, ` ```uml-archimate ` | макросы ArchiMate 3.2, без обёртки | PlantUML + stdlib ArchiMate |
|
||||||
|
| ` ```mermaid `, ` ```mmd ` | синтаксис Mermaid | локальный Mermaid или Kroki |
|
||||||
|
| ` ```idef0 `, ` ```uml-idef0 ` | DSL IDEF0 | локальный рендер (Pillow) |
|
||||||
|
| ` ```dfd `, ` ```uml-dfd `, ` ```data-flow-diagram `, ` ```yourdon ` | DSL потоков данных | Graphviz или Kroki |
|
||||||
|
|
||||||
|
Правило про обёртку: в ` ```uml ` пишешь `@startuml` … `@enduml` **обязательно**; в схемах (`c4`, `usecase`, `archimate` и любых из `md2gost.schemes.json`) — **никогда**, схема подставит обёртку и `!include` сама.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. PlantUML: готовые каркасы
|
||||||
|
|
||||||
|
### Диаграмма прецедентов
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
%uc_main Диаграмма прецедентов системы
|
||||||
|
|
||||||
|
```uml
|
||||||
|
@startuml
|
||||||
|
left to right direction
|
||||||
|
skinparam packageStyle rectangle
|
||||||
|
|
||||||
|
actor "Студент" as student
|
||||||
|
actor "Преподаватель" as teacher
|
||||||
|
actor "Администратор" as admin
|
||||||
|
|
||||||
|
rectangle "Портал заявок" {
|
||||||
|
usecase "Подать заявку" as UC1
|
||||||
|
usecase "Проверить статус" as UC2
|
||||||
|
usecase "Согласовать заявку" as UC3
|
||||||
|
usecase "Аутентифицироваться" as UC4
|
||||||
|
usecase "Управлять справочниками" as UC5
|
||||||
|
}
|
||||||
|
|
||||||
|
student --> UC1
|
||||||
|
student --> UC2
|
||||||
|
teacher --> UC3
|
||||||
|
admin --> UC5
|
||||||
|
UC1 ..> UC4 : <<include>>
|
||||||
|
UC3 ..> UC4 : <<include>>
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
~~~
|
||||||
|
|
||||||
|
### Диаграмма классов
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
%class_domain Диаграмма классов предметной области
|
||||||
|
|
||||||
|
```uml
|
||||||
|
@startuml
|
||||||
|
skinparam classAttributeIconSize 0
|
||||||
|
|
||||||
|
class Request {
|
||||||
|
- id: int
|
||||||
|
- createdAt: datetime
|
||||||
|
- status: Status
|
||||||
|
+ submit(): void
|
||||||
|
+ cancel(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
class Client {
|
||||||
|
- id: int
|
||||||
|
- name: str
|
||||||
|
- inn: str
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Status {
|
||||||
|
NEW
|
||||||
|
IN_PROGRESS
|
||||||
|
DONE
|
||||||
|
}
|
||||||
|
|
||||||
|
class RequestService {
|
||||||
|
+ create(client: Client): Request
|
||||||
|
+ close(request: Request): void
|
||||||
|
}
|
||||||
|
|
||||||
|
Client "1" *-- "0..*" Request
|
||||||
|
Request --> Status
|
||||||
|
RequestService ..> Request
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
~~~
|
||||||
|
|
||||||
|
### Диаграмма последовательности
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
%seq_submit Последовательность обработки заявки
|
||||||
|
|
||||||
|
```uml
|
||||||
|
@startuml
|
||||||
|
autonumber
|
||||||
|
actor Студент as s
|
||||||
|
participant "Веб-клиент" as web
|
||||||
|
participant "Сервис заявок" as api
|
||||||
|
database "PostgreSQL" as db
|
||||||
|
|
||||||
|
s -> web : заполняет форму
|
||||||
|
web -> api : POST /requests
|
||||||
|
api -> db : INSERT request
|
||||||
|
db --> api : id
|
||||||
|
api --> web : 201 Created
|
||||||
|
web --> s : номер заявки
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
~~~
|
||||||
|
|
||||||
|
### Диаграмма деятельности
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
%act_flow Алгоритм обработки заявки
|
||||||
|
|
||||||
|
```uml
|
||||||
|
@startuml
|
||||||
|
start
|
||||||
|
:Принять заявку;
|
||||||
|
if (Данные полные?) then (да)
|
||||||
|
:Зарегистрировать;
|
||||||
|
:Назначить исполнителя;
|
||||||
|
else (нет)
|
||||||
|
:Вернуть на доработку;
|
||||||
|
stop
|
||||||
|
endif
|
||||||
|
:Исполнить заявку;
|
||||||
|
:Уведомить клиента;
|
||||||
|
stop
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
~~~
|
||||||
|
|
||||||
|
### Диаграмма состояний
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
%state_req Состояния заявки
|
||||||
|
|
||||||
|
```uml
|
||||||
|
@startuml
|
||||||
|
[*] --> Новая
|
||||||
|
Новая --> ВРаботе : назначен исполнитель
|
||||||
|
ВРаботе --> Исполнена : работы завершены
|
||||||
|
ВРаботе --> Отклонена : отказ
|
||||||
|
Исполнена --> [*]
|
||||||
|
Отклонена --> [*]
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
~~~
|
||||||
|
|
||||||
|
### ER-диаграмма
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
%erd_main Логическая модель данных
|
||||||
|
|
||||||
|
```uml
|
||||||
|
@startuml
|
||||||
|
hide circle
|
||||||
|
skinparam linetype ortho
|
||||||
|
|
||||||
|
entity "Клиент" as client {
|
||||||
|
* id : bigint <<PK>>
|
||||||
|
--
|
||||||
|
* name : varchar(255)
|
||||||
|
inn : varchar(12)
|
||||||
|
}
|
||||||
|
|
||||||
|
entity "Заявка" as request {
|
||||||
|
* id : bigint <<PK>>
|
||||||
|
--
|
||||||
|
* client_id : bigint <<FK>>
|
||||||
|
* created_at : timestamp
|
||||||
|
* status : varchar(32)
|
||||||
|
}
|
||||||
|
|
||||||
|
entity "Позиция заявки" as item {
|
||||||
|
* id : bigint <<PK>>
|
||||||
|
--
|
||||||
|
* request_id : bigint <<FK>>
|
||||||
|
* product : varchar(255)
|
||||||
|
* qty : int
|
||||||
|
}
|
||||||
|
|
||||||
|
client ||--o{ request
|
||||||
|
request ||--o{ item
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
~~~
|
||||||
|
|
||||||
|
### Диаграмма компонентов и развёртывания
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
%deploy_prod Схема развёртывания
|
||||||
|
|
||||||
|
```uml
|
||||||
|
@startuml
|
||||||
|
node "Сервер приложений" {
|
||||||
|
component "API заявок" as api
|
||||||
|
component "Планировщик" as sched
|
||||||
|
}
|
||||||
|
node "Сервер БД" {
|
||||||
|
database "PostgreSQL" as db
|
||||||
|
}
|
||||||
|
node "Рабочее место" {
|
||||||
|
component "Браузер" as browser
|
||||||
|
}
|
||||||
|
|
||||||
|
browser --> api : HTTPS
|
||||||
|
api --> db : SQL
|
||||||
|
sched --> db : SQL
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
~~~
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. C4
|
||||||
|
|
||||||
|
Три уровня — три оградки. В теле только макросы; обёртка и `!include` добавляются схемой.
|
||||||
|
|
||||||
|
### Уровень 1: контекст
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
%c4ctx Диаграмма контекста системы
|
||||||
|
|
||||||
|
```uml-c4context
|
||||||
|
Person(student, "Студент", "Подаёт заявки и отслеживает статус")
|
||||||
|
Person(staff, "Сотрудник деканата", "Обрабатывает заявки")
|
||||||
|
System(portal, "Портал заявок", "Приём и обработка заявок студентов")
|
||||||
|
System_Ext(sso, "Корпоративный SSO", "Единая аутентификация")
|
||||||
|
System_Ext(mail, "Почтовый сервис", "Уведомления")
|
||||||
|
|
||||||
|
Rel(student, portal, "Подаёт заявку", "HTTPS")
|
||||||
|
Rel(staff, portal, "Обрабатывает заявки", "HTTPS")
|
||||||
|
Rel(portal, sso, "Аутентифицирует пользователей", "OIDC")
|
||||||
|
Rel(portal, mail, "Отправляет уведомления", "SMTP")
|
||||||
|
```
|
||||||
|
~~~
|
||||||
|
|
||||||
|
### Уровень 2: контейнеры
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
%c4cont Диаграмма контейнеров +landscape
|
||||||
|
|
||||||
|
```uml-c4
|
||||||
|
Person(student, "Студент", "Подаёт заявки")
|
||||||
|
System_Boundary(portal, "Портал заявок") {
|
||||||
|
Container(spa, "Веб-приложение", "React", "Интерфейс пользователя")
|
||||||
|
Container(api, "Сервис заявок", "FastAPI", "Бизнес-логика и валидация")
|
||||||
|
Container(worker, "Обработчик очереди", "Python", "Асинхронные задачи")
|
||||||
|
ContainerDb(db, "Основная БД", "PostgreSQL", "Заявки, клиенты, история")
|
||||||
|
ContainerDb(queue, "Очередь", "RabbitMQ", "Задачи на обработку")
|
||||||
|
}
|
||||||
|
System_Ext(sso, "Корпоративный SSO", "Аутентификация")
|
||||||
|
|
||||||
|
Rel(student, spa, "Работает с заявками", "HTTPS")
|
||||||
|
Rel(spa, api, "Вызывает методы", "REST/JSON")
|
||||||
|
Rel(api, db, "Читает и пишет", "SQL")
|
||||||
|
Rel(api, queue, "Публикует задачи", "AMQP")
|
||||||
|
Rel(worker, queue, "Читает задачи", "AMQP")
|
||||||
|
Rel(worker, db, "Обновляет статусы", "SQL")
|
||||||
|
Rel(spa, sso, "Аутентифицируется", "OIDC")
|
||||||
|
```
|
||||||
|
~~~
|
||||||
|
|
||||||
|
### Уровень 3: компоненты
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
%c4comp Компоненты сервиса заявок
|
||||||
|
|
||||||
|
```uml-c4component
|
||||||
|
Container_Boundary(api, "Сервис заявок") {
|
||||||
|
Component(ctrl, "Контроллер заявок", "FastAPI Router", "Обработка HTTP-запросов")
|
||||||
|
Component(svc, "Служба заявок", "Python", "Правила обработки и переходы статусов")
|
||||||
|
Component(repo, "Репозиторий заявок", "SQLAlchemy", "Доступ к данным")
|
||||||
|
Component(auth, "Модуль авторизации", "Python", "Проверка прав по роли")
|
||||||
|
}
|
||||||
|
ContainerDb(db, "Основная БД", "PostgreSQL", "Заявки и клиенты")
|
||||||
|
|
||||||
|
Rel(ctrl, auth, "Проверяет права")
|
||||||
|
Rel(ctrl, svc, "Вызывает")
|
||||||
|
Rel(svc, repo, "Читает и пишет")
|
||||||
|
Rel(repo, db, "SQL-запросы")
|
||||||
|
```
|
||||||
|
~~~
|
||||||
|
|
||||||
|
### Основные макросы C4
|
||||||
|
|
||||||
|
| Макрос | Назначение |
|
||||||
|
|--------|------------|
|
||||||
|
| `Person(alias, "Имя", "Описание")` | пользователь-человек |
|
||||||
|
| `Person_Ext(...)` | внешний пользователь |
|
||||||
|
| `System(alias, "Имя", "Описание")` | система целиком |
|
||||||
|
| `System_Ext(...)` | внешняя система |
|
||||||
|
| `System_Boundary(alias, "Имя") { … }` | граница системы |
|
||||||
|
| `Container(alias, "Имя", "Технология", "Описание")` | контейнер |
|
||||||
|
| `ContainerDb(...)`, `ContainerQueue(...)` | база данных, очередь |
|
||||||
|
| `Container_Boundary(alias, "Имя") { … }` | граница контейнера |
|
||||||
|
| `Component(alias, "Имя", "Технология", "Описание")` | компонент |
|
||||||
|
| `Rel(from, to, "Действие", "Технология")` | связь |
|
||||||
|
| `Rel_U/D/L/R(...)` | связь с направлением раскладки |
|
||||||
|
| `LAYOUT_WITH_LEGEND()`, `LAYOUT_TOP_DOWN()`, `LAYOUT_LEFT_RIGHT()` | раскладка |
|
||||||
|
|
||||||
|
Правила качества: описания на русском, технологии — как принято (`PostgreSQL`, `React`); один уровень абстракции на диаграмму; 6–15 элементов; связи подписаны действием, а не «использует».
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Mermaid
|
||||||
|
|
||||||
|
Идёт через локальный рендер (системный Chrome/Edge, Playwright Chromium или QuickJS) либо через Kroki.
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
%mmd_flow Схема процесса согласования
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[Заявка поступила] --> B{Данные полные?}
|
||||||
|
B -- да --> C[Регистрация]
|
||||||
|
B -- нет --> D[Возврат на доработку]
|
||||||
|
C --> E[Назначение исполнителя]
|
||||||
|
E --> F[Исполнение]
|
||||||
|
F --> G[Уведомление клиента]
|
||||||
|
D --> A
|
||||||
|
```
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Поддерживаются `flowchart`, `sequenceDiagram`, `classDiagram`, `stateDiagram-v2`, `erDiagram`, `gantt`, `journey`, `pie`. Для отчёта по ГОСТ надёжнее PlantUML: Mermaid требует браузера или сети.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. IDEF0 (FIPS 183)
|
||||||
|
|
||||||
|
Оградка ` ```idef0 `. Тело — не XML и не PlantUML, а собственный DSL. Рендер локальный, Java и сеть не нужны.
|
||||||
|
|
||||||
|
### Шапка формы
|
||||||
|
|
||||||
|
| Ключ | Значение |
|
||||||
|
|------|----------|
|
||||||
|
| `title` | название диаграммы |
|
||||||
|
| `node` | узел: `A-0`, `A0`, `A1` |
|
||||||
|
| `number` | номер листа |
|
||||||
|
| `author`, `project`, `date`, `rev` | поля NIST-формы |
|
||||||
|
| `status` | `WORKING`, `DRAFT`, `RECOMMENDED`, `PUBLICATION` |
|
||||||
|
| `context` | `TOP` для контекстной A-0, иначе родительский узел |
|
||||||
|
| `purpose`, `viewpoint` | цель и точка зрения модели |
|
||||||
|
| `used_at`, `reader`, `reader_date`, `notes`, `page` | необязательные поля формы |
|
||||||
|
| `form` | `kit` (по умолчанию, с рамкой) или `plain` |
|
||||||
|
|
||||||
|
### Боксы и стрелки
|
||||||
|
|
||||||
|
| Запись | Смысл |
|
||||||
|
|--------|-------|
|
||||||
|
| `[A0] Имя функции` или `box A0 Имя` | бокс функции |
|
||||||
|
| `<- Название` или `in` | вход (слева) |
|
||||||
|
| `^ Название` или `control` | управление (сверху) |
|
||||||
|
| `-> Название` или `out` | выход (справа) |
|
||||||
|
| `v Название` или `mech` | механизм (снизу) |
|
||||||
|
| `A1 <- Заявка [I1]` | явное указание бокса и код ICOM |
|
||||||
|
| `A1 -> A2 : план закупок` | внутренняя связь между боксами |
|
||||||
|
| `A1 -> A2.C : график` | связь в конкретный порт (`.I`, `.C`, `.O`, `.M`) |
|
||||||
|
| `A1, A2, A3 ^ Регламент [C1]` | одна стрелка в несколько боксов |
|
||||||
|
| `(in Секрет` | туннель на границе листа |
|
||||||
|
| `in) Локальный` | туннель у бокса |
|
||||||
|
| `call ИмяМодели` | вызов другой модели |
|
||||||
|
|
||||||
|
Стрелки без явного бокса относятся к последнему объявленному боксу.
|
||||||
|
|
||||||
|
### Контекстная диаграмма A-0
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
%idef_a0 Контекстная диаграмма A-0
|
||||||
|
|
||||||
|
```idef0
|
||||||
|
title Распорядиться товаром
|
||||||
|
node A-0
|
||||||
|
number 1
|
||||||
|
author Иванов И. И.
|
||||||
|
project Модель торгового предприятия
|
||||||
|
date 08.02.2026
|
||||||
|
status PUBLICATION
|
||||||
|
context TOP
|
||||||
|
|
||||||
|
[A0] Распорядиться товаром
|
||||||
|
<- Спрос
|
||||||
|
^ Нормативная документация
|
||||||
|
-> Товар
|
||||||
|
v Персонал
|
||||||
|
```
|
||||||
|
~~~
|
||||||
|
|
||||||
|
### Декомпозиция A0
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
%idef_a0d Декомпозиция A0
|
||||||
|
|
||||||
|
```idef0
|
||||||
|
title Распорядиться товаром
|
||||||
|
node A0
|
||||||
|
number 2
|
||||||
|
author Иванов И. И.
|
||||||
|
project Модель торгового предприятия
|
||||||
|
context A-0
|
||||||
|
purpose Обеспечить наличие товара и его отпуск покупателю
|
||||||
|
viewpoint Руководитель торгового предприятия
|
||||||
|
|
||||||
|
[A1] Принять и оценить спрос
|
||||||
|
[A2] Закупить и хранить
|
||||||
|
[A3] Отгрузить товар
|
||||||
|
|
||||||
|
A1 <- Спрос [I1]
|
||||||
|
A2 <- Предложения поставщиков [I2]
|
||||||
|
A1, A2, A3 ^ Нормативная документация [C1]
|
||||||
|
A1 ^ Ассортиментная политика [C2]
|
||||||
|
A3 ^ Условия поставки [C3]
|
||||||
|
A1 -> A2 : план закупок
|
||||||
|
A1 -> A2.C : график поставок
|
||||||
|
A2 -> A3 : товар к отгрузке
|
||||||
|
A3 -> Товар [O1]
|
||||||
|
A3 -> Акт отгрузки [O2]
|
||||||
|
A1, A2, A3 v Персонал [M1]
|
||||||
|
A2 v Склад [M2]
|
||||||
|
```
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Правила нотации: контекстная диаграмма — ровно один бокс; в декомпозиции 3–6 боксов «лесенкой» слева-сверху вниз-вправо; подписи стрелок — существительные; управление приходит сверху, механизм снизу.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. DFD (диаграммы потоков данных)
|
||||||
|
|
||||||
|
Оградка ` ```dfd `. Синтаксис пакета `data-flow-diagram`. Рендер через Graphviz (вшитый, из `PATH` или скачанный) либо Kroki.
|
||||||
|
|
||||||
|
### Узлы и потоки
|
||||||
|
|
||||||
|
| Ключевое слово | Смысл |
|
||||||
|
|----------------|-------|
|
||||||
|
| `process Имя Подпись` | процесс |
|
||||||
|
| `entity Имя Подпись` | внешняя сущность |
|
||||||
|
| `store Имя Подпись` | хранилище данных |
|
||||||
|
| `control Имя Подпись` | элемент управления |
|
||||||
|
| `channel Имя Подпись` | канал |
|
||||||
|
| `style context` | оформление контекстной диаграммы |
|
||||||
|
|
||||||
|
| Поток | Смысл |
|
||||||
|
|-------|-------|
|
||||||
|
| `A --> B подпись` | поток данных |
|
||||||
|
| `A ->> B подпись` | двойной поток |
|
||||||
|
| `A <-> B подпись` | двунаправленный |
|
||||||
|
| `A ::> B подпись` | сигнал |
|
||||||
|
|
||||||
|
### Контекстная диаграмма
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
%dfd_ctx Контекстная диаграмма потоков данных
|
||||||
|
|
||||||
|
```dfd
|
||||||
|
style context
|
||||||
|
|
||||||
|
entity Client Клиент
|
||||||
|
entity Warehouse Склад
|
||||||
|
process System Система учёта заявок
|
||||||
|
|
||||||
|
Client --> System заявка
|
||||||
|
System --> Client статус заявки
|
||||||
|
System --> Warehouse накладная
|
||||||
|
```
|
||||||
|
~~~
|
||||||
|
|
||||||
|
### Диаграмма нулевого уровня
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
%dfd_l0 Диаграмма потоков данных нулевого уровня
|
||||||
|
|
||||||
|
```dfd
|
||||||
|
entity Client Клиент
|
||||||
|
entity Warehouse Склад
|
||||||
|
process Accept Принять заявку
|
||||||
|
process Register Зарегистрировать заявку
|
||||||
|
process Ship Сформировать накладную
|
||||||
|
store Orders Заявки
|
||||||
|
store Log Журнал операций
|
||||||
|
|
||||||
|
Client --> Accept заявка
|
||||||
|
Accept --> Orders новая заявка
|
||||||
|
Orders --> Register данные заявки
|
||||||
|
Register --> Log запись об изменении
|
||||||
|
Register --> Ship подтверждённая заявка
|
||||||
|
Ship --> Warehouse накладная
|
||||||
|
Ship --> Client уведомление
|
||||||
|
```
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Правила нотации: на контекстной диаграмме один процесс; хранилища не соединяются напрямую с внешними сущностями; каждый процесс имеет и вход, и выход.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Что нужно для отрисовки
|
||||||
|
|
||||||
|
| Нотация | Требования | Запасной вариант |
|
||||||
|
|---------|------------|------------------|
|
||||||
|
| PlantUML, C4, ArchiMate, Use Case | Java и `plantuml.jar` (вшит в exe-сборку) | локальный Kroki, затем `kroki.io` при `--diagram-fallback remote` |
|
||||||
|
| Mermaid | системный Chrome или Edge; либо скачанный Chromium; либо QuickJS | Kroki |
|
||||||
|
| IDEF0 | ничего, локальный рендер | не требуется |
|
||||||
|
| DFD | Graphviz (вшитый, из `PATH` или кнопка «Скачать Graphviz») | Kroki |
|
||||||
|
|
||||||
|
Ключи: `--plantuml-jar`, `--kroki-url`, `--diagram-fallback local|remote|off`, `--diagram-format png|svg`, `--diagram-scale N` (по умолчанию 2 — качество растра).
|
||||||
|
|
||||||
|
Кэш готовых картинок — каталог `.md2gost-cache` рядом с `.md`; при правке исходника схемы картинка пересобирается автоматически.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Свои схемы
|
||||||
|
|
||||||
|
Файл `md2gost.schemes.json` (рядом с приложением, рядом с `.md` или путь в `--schemes`) задаёт новые оградки:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"bpmn_like": {
|
||||||
|
"title": "Процессная схема",
|
||||||
|
"docs": "Шпаргалка макросов для подсказки в интерфейсе",
|
||||||
|
"ai-prompt": "Фрагмент, который дописывается в промпт для ИИ",
|
||||||
|
"includes": ["https://example.org/macros.puml"],
|
||||||
|
"prefix": "@startuml\n",
|
||||||
|
"postfix": "\n@enduml",
|
||||||
|
"theme": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`id` схемы: строчная латиница, цифры, `_`, начинается с буквы. После добавления доступны оградки ` ```bpmn_like ` и ` ```uml-bpmn_like `; в теле пишутся только макросы.
|
||||||
|
|
||||||
|
Слои загрузки, где каждый следующий сильнее: встроенный шаблон → файл рядом с приложением → файл рядом с `.md` → `--schemes`.
|
||||||
@@ -0,0 +1,459 @@
|
|||||||
|
# Диалект Markdown md2gost — полная спецификация
|
||||||
|
|
||||||
|
Справочник синтаксиса конвертера md2gost (Markdown → DOCX по ГОСТ 7.32-2017 и ТЗ РТУ МИРЭА). Всё, что здесь описано, извлечено из кода парсера, нумератора и проверки, а не из пользовательских инструкций.
|
||||||
|
|
||||||
|
Базовый парсер — Marko с расширением GFM. Всё, что не перечислено ниже как расширение, работает как обычный GitHub Flavored Markdown.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Расширения относительно обычного Markdown
|
||||||
|
|
||||||
|
| Конструкция | Смысл |
|
||||||
|
|-------------|-------|
|
||||||
|
| `# *ЗАГОЛОВОК` | раздел без автоматической нумерации |
|
||||||
|
| `[TOC]` | точка сборки содержания |
|
||||||
|
| `%id Подпись` | метка и подпись следующего объекта |
|
||||||
|
| `` | метка и подпись рисунка-файла |
|
||||||
|
| `@Тип:id` | перекрёстная ссылка с автоподстановкой номера |
|
||||||
|
| `[1]: описание` | запись списка источников |
|
||||||
|
| `[1]`, `[2, 3]`, `[2, c. 21-25]` | ссылка на источник в тексте |
|
||||||
|
| `^` / `>` в ячейке таблицы | объединение ячеек |
|
||||||
|
| ` ```uml `, ` ```mermaid `, ` ```idef0 `, ` ```dfd `, ` ```uml-<схема>` | диаграмма, отрисовываемая конвертером |
|
||||||
|
| ` ```title ` | данные для автогенерации титульного листа |
|
||||||
|
| `+listing`, `+landscape` в строке `%` | флаги оформления объекта |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Заголовки
|
||||||
|
|
||||||
|
Синтаксис: от нуля до трёх ведущих пробелов, затем `#`…`######`, пробел, текст.
|
||||||
|
|
||||||
|
Регулярное выражение парсера: `^ {0,3}(#{1,6})((?=\s)[^\n]*?|[^\n\S]*)(?:(?<=\s)(?<!\\)#+)?[^\n\S]*$`
|
||||||
|
|
||||||
|
**Звёздочка** первым непробельным символом текста заголовка делает раздел ненумерованным и удаляется из видимого текста:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# *ВВЕДЕНИЕ → раздел без номера
|
||||||
|
# 1 Анализ → нумерованный раздел
|
||||||
|
```
|
||||||
|
|
||||||
|
Пробел после `#` обязателен для корректной работы проверки (`^(#{1,6})\s+(\*?)(.+)$`).
|
||||||
|
|
||||||
|
### Спецразделы
|
||||||
|
|
||||||
|
Распознаются по тексту заголовка, приведённому к прописным. Список из кода проверки:
|
||||||
|
|
||||||
|
`СОДЕРЖАНИЕ`, `ВВЕДЕНИЕ`, `ЗАКЛЮЧЕНИЕ`, `СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ`, `СПИСОК ИСПОЛЬЗУЕМЫХ ИСТОЧНИКОВ`, `ПРИЛОЖЕНИЕ`, `ПРИЛОЖЕНИЯ`, а также любой заголовок, начинающийся на `ПРИЛОЖЕН`.
|
||||||
|
|
||||||
|
Для H1 из этого набора звёздочка обязательна — иначе ошибка `heading.numbered_special`.
|
||||||
|
|
||||||
|
Оформление в DOCX:
|
||||||
|
|
||||||
|
| Заголовок H1 | Выравнивание |
|
||||||
|
|--------------|--------------|
|
||||||
|
| `СОДЕРЖАНИЕ`, `СПИСОК ИСПОЛЬЗОВАННЫХ/ИСПОЛЬЗУЕМЫХ ИСТОЧНИКОВ` | по центру |
|
||||||
|
| `ВВЕДЕНИЕ`, `ЗАКЛЮЧЕНИЕ`, `ПРИЛОЖЕНИЯ`, нумерованные разделы | слева с абзацным отступом |
|
||||||
|
|
||||||
|
`СОДЕРЖАНИЕ` исключается из собираемого оглавления.
|
||||||
|
|
||||||
|
### Нумерация заголовков
|
||||||
|
|
||||||
|
| Режим CLI | Поведение |
|
||||||
|
|-----------|-----------|
|
||||||
|
| `--heading-numbering manual` (по умолчанию) | цифры остаются из markdown: `# 1 …`, `## 1.1 …`; автонумерация Word отключена |
|
||||||
|
| `--heading-numbering auto` | ведущие цифры (`^\d+(?:\.\d+)*\.?\s+`) снимаются с текста заголовка, номера ставит многоуровневый список Word |
|
||||||
|
|
||||||
|
При `manual` номера пиши сам. При `auto` их можно не писать — но безопаснее всегда писать и запускать конвертер в `manual`.
|
||||||
|
|
||||||
|
### `[TOC]`
|
||||||
|
|
||||||
|
Блочный элемент, шаблон `\[(TOC)\]+`. **Только прописными** — `[toc]` не распарсится (хотя проверка его увидит и замечания не выдаст). Ставится отдельной строкой, обычно сразу под `# *СОДЕРЖАНИЕ`.
|
||||||
|
|
||||||
|
Режимы сборки: `--toc native` (по умолчанию, поле Word, обновляется при открытии документа) и `--toc manual` (md2gost сам считает страницы).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Метки объектов: строка `%`
|
||||||
|
|
||||||
|
Шаблон: `\%(\w+)( (.+))?` — блочный элемент, прерывает открытый абзац.
|
||||||
|
|
||||||
|
- `id` = `\w+`: буквы, цифры, `_`. **Дефис, точка, пробел внутри `id` недопустимы.** Практическое правило: `[A-Za-z][A-Za-z0-9_]*`.
|
||||||
|
- Подпись необязательна: `%eq1` — валидная строка.
|
||||||
|
- Метка применяется к **следующему непустому блоку**. Пустые строки между меткой и объектом допустимы.
|
||||||
|
- Если подряд идут две метки, к объекту привяжется вторая.
|
||||||
|
- `%` в середине абзаца — обычный текст, не метка.
|
||||||
|
|
||||||
|
### Флаги в строке подписи
|
||||||
|
|
||||||
|
Шаблоны: `(?i)(?:^|\s)\+?listing\b` и `(?i)(?:^|\s)\+?landscape\b`.
|
||||||
|
|
||||||
|
| Флаг | Действие |
|
||||||
|
|------|----------|
|
||||||
|
| `+listing` | у диаграммы дополнительно выводится Листинг с её исходником (получает свой номер) |
|
||||||
|
| `+landscape` | объект размещается на альбомной странице |
|
||||||
|
|
||||||
|
Важные следствия из регулярных выражений:
|
||||||
|
|
||||||
|
1. Плюс необязателен: `listing` без плюса тоже сработает как флаг.
|
||||||
|
2. Регистр не важен: `+Landscape` = `+landscape`.
|
||||||
|
3. Флаги вырезаются из текста подписи в любом месте строки. **Поэтому подпись не должна содержать самостоятельных слов `listing` и `landscape`** — они исчезнут из подписи, а объект получит нежелательное оформление.
|
||||||
|
|
||||||
|
### Метка рисунка-файла
|
||||||
|
|
||||||
|
Метка задаётся в title изображения:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|

|
||||||
|
```
|
||||||
|
|
||||||
|
Тот же шаблон `%(\w+)( (.+))?` и те же флаги.
|
||||||
|
|
||||||
|
### Куда попадает подпись
|
||||||
|
|
||||||
|
| Объект | Положение подписи в DOCX |
|
||||||
|
|--------|--------------------------|
|
||||||
|
| Рисунок и диаграмма | под объектом |
|
||||||
|
| Таблица | надпись над таблицей |
|
||||||
|
| Листинг | надпись над листингом |
|
||||||
|
| Формула | номер в скобках справа от формулы |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Перекрёстные ссылки `@Тип:id`
|
||||||
|
|
||||||
|
Шаблон: `@(\w+):(\w+)`. Пробелы вокруг двоеточия недопустимы. Знаки препинания после ссылки не съедаются, поэтому `@Рисунок:arch.` и `@Таблица:cmp,` работают корректно.
|
||||||
|
|
||||||
|
Принимаемые типы (регистр не важен при разрешении ссылки):
|
||||||
|
|
||||||
|
| Тип | Синонимы |
|
||||||
|
|-----|----------|
|
||||||
|
| `Рисунок` | `figure` |
|
||||||
|
| `Таблица` | `table` |
|
||||||
|
| `Листинг` | `listing` |
|
||||||
|
| `Формула` | `formula`, `equation` |
|
||||||
|
|
||||||
|
В документе ссылка разворачивается в «Тип Номер», например `Рисунок 1.1`. Неизвестная метка даёт в тексте `Рисунок?` — визуальный маркер ошибки; проверка сообщает `ref.dangling`.
|
||||||
|
|
||||||
|
Каноническая форма — русские названия: `@Рисунок:id`, `@Таблица:id`, `@Листинг:id`, `@Формула:id`.
|
||||||
|
|
||||||
|
### Соответствие меток и ссылок
|
||||||
|
|
||||||
|
Проверка сверяет два множества:
|
||||||
|
|
||||||
|
- метки — из `^%(\w+)` и из title картинок;
|
||||||
|
- ссылки — из `@[\wА-Яа-я]+:(\w+)`.
|
||||||
|
|
||||||
|
Метка без ссылки → предупреждение `ref.unused`. Ссылка без метки → ошибка `ref.dangling`. Правило простое: **сколько меток, столько и адресатов ссылок**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Нумерация объектов
|
||||||
|
|
||||||
|
Области нумерации задаются профилем `--type`:
|
||||||
|
|
||||||
|
| Область | Профили | Вид номера |
|
||||||
|
|---------|---------|------------|
|
||||||
|
| `section` | `practice`, `coursework`, `vkr`, `APID_coursework` | `1.1`, `2.3`; в приложении — `Б.1` |
|
||||||
|
| `continuous` | `PIS_custom` | `1`, `2`, `3` на весь документ |
|
||||||
|
|
||||||
|
Счётчик раздела увеличивается только на **нумерованных H1** (то есть на `# 1 …`, а не на `# *ВВЕДЕНИЕ`).
|
||||||
|
|
||||||
|
Номер получают все объекты **кроме формул**: формуле номер присваивается, только если в исходном тексте есть `@Формула:id` (или `@Formula:id`) с её меткой. Формула без ссылки остаётся без номера.
|
||||||
|
|
||||||
|
Диаграмма с флагом `+listing` получает два номера под одной меткой: номер Рисунка и номер Листинга. Ссылки `@Рисунок:id` и `@Листинг:id` разрешатся каждая в свой номер.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Таблицы
|
||||||
|
|
||||||
|
Синтаксис GFM: строка заголовков, строка-разделитель, строки данных. Выравнивание через `:---`, `:---:`, `---:` поддерживается. Экранированный `\|` не считается границей столбца.
|
||||||
|
|
||||||
|
### Объединение ячеек
|
||||||
|
|
||||||
|
| Маркер | Смысл |
|
||||||
|
|--------|-------|
|
||||||
|
| `^` или `^^` | ячейка — продолжение ячейки **сверху** (rowspan) |
|
||||||
|
| `>` или `>>` | ячейка — продолжение ячейки **слева** (colspan) |
|
||||||
|
|
||||||
|
Маркер должен быть **единственным** содержимым ячейки после обрезки пробелов. Ограничения проверки: маркеры запрещены в строке заголовков (`table.merge_header`), `>` запрещён в первом столбце (`table.merge_col`).
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
%tab_req Требования к системе
|
||||||
|
|
||||||
|
| Категория | Требование |
|
||||||
|
|-----------|------------|
|
||||||
|
| Производительность | обработка 1000 заявок в час |
|
||||||
|
| ^ | время отклика не более 2 с |
|
||||||
|
| Надёжность | доступность 99,5 % |
|
||||||
|
```
|
||||||
|
|
||||||
|
### Правила ТЗ
|
||||||
|
|
||||||
|
- Графа «№ п/п» запрещена (`table.nopp`, шаблон `№\s*п\s*/\s*п`).
|
||||||
|
- Таблицу из одной строки данных не оформляют таблицей.
|
||||||
|
- Таблицы и рисунки не размещают внутри списков.
|
||||||
|
|
||||||
|
### Продолжение на следующей странице
|
||||||
|
|
||||||
|
DOCX не умеет сам подписывать «Продолжение Таблицы N». Режимы `--table-continuation` и `--listing-continuation`: `word` (по умолчанию; после сохранения Word COM режет по реальной пагинации), `off`, `soft`, `legacy`, `caption`. Режим `word` требует Windows, Microsoft Word и пакет `pywin32`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Листинги
|
||||||
|
|
||||||
|
Любой fenced-блок с языком, который **не** является языком диаграммы, становится Листингом.
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
%code_volume Процедура расчёта объёма
|
||||||
|
|
||||||
|
```python
|
||||||
|
def volume(count: int, size: float) -> float:
|
||||||
|
return count * size
|
||||||
|
```
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Подсветка синтаксиса включается ключом `--syntax-highlighting` (по умолчанию выключена).
|
||||||
|
|
||||||
|
### Ловушка: автозамена внутри кода
|
||||||
|
|
||||||
|
Препроцессор применяет типографские правки ко всему тексту, **исключая только блоки диаграмм**. Блоки кода не защищены:
|
||||||
|
|
||||||
|
| В исходнике | В DOCX |
|
||||||
|
|-------------|--------|
|
||||||
|
| `print("hello")` | `print(«hello»)` |
|
||||||
|
| `x = a - b` | `x = a — b` |
|
||||||
|
|
||||||
|
Обход: одинарные кавычки в примерах кода и вычитание без пробелов (`a-b`). Ключ `--emdash-to-hyphen` меняет направление замены тире, но кавычки правит всегда.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Формулы
|
||||||
|
|
||||||
|
| Форма | Шаблон | Назначение |
|
||||||
|
|-------|--------|------------|
|
||||||
|
| Выключная | `\$\$([\S\s]*?)\$\$` | отдельный блок, нумеруемая формула |
|
||||||
|
| Инлайновая | `\$(.*?)\$` | обозначения в тексте: `$N$` |
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
%eq_volume
|
||||||
|
$$
|
||||||
|
V = N \times S
|
||||||
|
$$
|
||||||
|
|
||||||
|
где $N$ — число записей; $S$ — средний размер записи, байт.
|
||||||
|
|
||||||
|
Оценка по (@Формула:eq_volume) используется при выборе хранилища.
|
||||||
|
```
|
||||||
|
|
||||||
|
Номер присваивается только при наличии ссылки. Расшифровка обозначений начинается со слова «где» без двоеточия.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Диаграммы
|
||||||
|
|
||||||
|
Fenced-блок с языком диаграммы превращается в Рисунок; исходник отрисовывается конвертером.
|
||||||
|
|
||||||
|
### Встроенные языки
|
||||||
|
|
||||||
|
| Оградка | Движок |
|
||||||
|
|---------|--------|
|
||||||
|
| `uml`, `plantuml` | PlantUML (java + `plantuml.jar`, иначе Kroki) |
|
||||||
|
| `mermaid`, `mmd` | локальный Mermaid (браузер / QuickJS), иначе Kroki |
|
||||||
|
| `idef0`, `uml-idef0` | локальный рендер на Pillow, без Java и сети |
|
||||||
|
| `dfd`, `uml-dfd`, `data-flow-diagram`, `yourdon`, `uml-yourdon` | пакет `data-flow-diagram` + Graphviz, иначе Kroki |
|
||||||
|
|
||||||
|
### Схемы из `md2gost.schemes.json`
|
||||||
|
|
||||||
|
Оградка `uml-<id>` или просто `<id>`, где `<id>` — идентификатор схемы (`^[a-z][a-z0-9_]*$`). Встроенные схемы:
|
||||||
|
|
||||||
|
| id | Что это | Оградка |
|
||||||
|
|----|---------|---------|
|
||||||
|
| `c4` | C4 Container | ` ```uml-c4 ` / ` ```c4 ` |
|
||||||
|
| `c4context` | C4 System Context | ` ```uml-c4context ` |
|
||||||
|
| `c4component` | C4 Component | ` ```uml-c4component ` |
|
||||||
|
| `usecase` | UML Use Case | ` ```uml-usecase ` / ` ```usecase ` |
|
||||||
|
| `archimate` | ArchiMate 3.2 | ` ```uml-archimate ` / ` ```archimate ` |
|
||||||
|
|
||||||
|
В теле схемы пишутся **только макросы**: обёртку `@startuml` … `@enduml` и `!include` подставляет схема. В блоке ` ```uml ` наоборот — `@startuml` … `@enduml` обязательны.
|
||||||
|
|
||||||
|
Неизвестный `uml-<id>` даёт ошибку рендера: `Схема диаграммы «<id>» не найдена`.
|
||||||
|
|
||||||
|
Подробный синтаксис всех нотаций — в `diagrams.md`, ArchiMate — в `archimate.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Источники
|
||||||
|
|
||||||
|
### Запись списка
|
||||||
|
|
||||||
|
Шаблон строки: `^\[(\d+(?:\.\d+)?)\]:\s*(.+)$`
|
||||||
|
|
||||||
|
Записи размещаются под заголовком, совпадающим с `^#\s*\*?\s*СПИСОК\s+ИСПОЛЬЗОВАНН?ЫХ\s+ИСТОЧНИКОВ\s*$` — то есть «СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ» или «СПИСОК ИСПОЛЬЗУЕМЫХ ИСТОЧНИКОВ».
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# *СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ
|
||||||
|
|
||||||
|
[1]: Иванов И. И. Проектирование информационных систем: учебник. — М.: Инфра-М, 2023. — 320 с.
|
||||||
|
|
||||||
|
[2]: ГОСТ 7.32-2017. Отчёт о научно-исследовательской работе. — М.: Стандартинформ, 2018. — 32 с.
|
||||||
|
```
|
||||||
|
|
||||||
|
Препроцессор сам расставит пустые строки между записями, но безопаснее писать их сразу.
|
||||||
|
|
||||||
|
Для ВКР список делится подразделами `##`; ключ записи получает вид `[1.5]` — номер раздела и номер внутри раздела.
|
||||||
|
|
||||||
|
### Ссылка в тексте
|
||||||
|
|
||||||
|
Шаблон: `\[(\d+(?:\.\d+)?(?:\s*,\s*\d+(?:\.\d+)?)*)(?:\s*,\s*[cс]\.\s*[\d\-–]+)?\]`
|
||||||
|
|
||||||
|
| Форма | Пример |
|
||||||
|
|-------|--------|
|
||||||
|
| одиночная | `[1]`, `[1.5]` |
|
||||||
|
| перечисление | `[2, 3]`, `[2,3]` |
|
||||||
|
| с указанием страниц | `[2, c. 21-25]`, `[2.18, с. 21–25]` — буква `c` латинская или русская |
|
||||||
|
|
||||||
|
В DOCX номер становится внутренней гиперссылкой на запись списка.
|
||||||
|
|
||||||
|
Сноски `[^1]` запрещены (`footnote`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Приложения
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# *ПРИЛОЖЕНИЯ
|
||||||
|
|
||||||
|
Приложение А — Листинг модуля обработки заявок
|
||||||
|
Приложение Б — Схема базы данных
|
||||||
|
|
||||||
|
## Приложение А Листинг модуля обработки заявок
|
||||||
|
|
||||||
|
## Приложение Б Схема базы данных
|
||||||
|
```
|
||||||
|
|
||||||
|
- Заголовок приложения распознаётся шаблоном `^ПРИЛОЖЕНИЕ\s+([А-ЯA-ZЁ])(?:\s+(.+))?$` без учёта регистра. Оформляется как Heading 3 по центру с разрывом страницы; название — отдельной строкой под ним.
|
||||||
|
- Допустимые буквы: `АБВГДЕЖИКЛМНПРСТУФХЦШЩЭЮЯ`. Запрещённые: `Ё З Й О Ч Ь Ы Ъ` (ошибка `appendix.letter`).
|
||||||
|
- Если букву не указать или указать запрещённую, конвертер подставит следующую допустимую по порядку.
|
||||||
|
- При двух и более приложениях после `# *ПРИЛОЖЕНИЯ` нужен перечень обычным текстом, иначе предупреждение `appendix.toc`. Если перечня нет, конвертер вставит его сам, но лучше писать явно.
|
||||||
|
- Объекты внутри приложения нумеруются с буквой приложения: `Рисунок Б.1`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Блок ` ```title `
|
||||||
|
|
||||||
|
Первый блок с оградкой `title` в файле вырезается до разбора markdown и используется для автогенерации титульного листа (только при `--auto-title`).
|
||||||
|
|
||||||
|
````markdown
|
||||||
|
```title
|
||||||
|
number: 1
|
||||||
|
year: 2026
|
||||||
|
```
|
||||||
|
|
||||||
|
# *СОДЕРЖАНИЕ
|
||||||
|
|
||||||
|
[TOC]
|
||||||
|
````
|
||||||
|
|
||||||
|
Тело — плоские строки `ключ: значение`. Принимаемые ключи:
|
||||||
|
|
||||||
|
| Поле | Ключи |
|
||||||
|
|------|-------|
|
||||||
|
| Институт | `institute`, `институт`, `инститиут` |
|
||||||
|
| Кафедра | `department`, `кафедра` |
|
||||||
|
| Дисциплина | `discipline`, `дисциплина` |
|
||||||
|
| Преподаватель | `teacher`, `преподаватель`, `преподователь` |
|
||||||
|
| Должность преподавателя | `teacher_position`, `должность`, `должность преподавателя` |
|
||||||
|
| Номер работы | `number`, `номер`, `№`, `n` — по умолчанию `1` |
|
||||||
|
| Год | `year`, `год` — по умолчанию текущий |
|
||||||
|
|
||||||
|
ФИО и группа студента **из блока не читаются**: они берутся из профиля приложения или ключей `--student` / `--group`.
|
||||||
|
|
||||||
|
Остальные академические поля можно держать в файле `info_conv.yaml` рядом с `.md`; значения из блока ` ```title ` сильнее.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Автоматические правки текста
|
||||||
|
|
||||||
|
Препроцессор применяет ко всему тексту, **кроме блоков диаграмм**:
|
||||||
|
|
||||||
|
| Правило | Было | Стало |
|
||||||
|
|---------|------|-------|
|
||||||
|
| Русские кавычки | `"текст"` | `«текст»` |
|
||||||
|
| Тире вместо дефиса между словами | `слово - слово` | `слово — слово` |
|
||||||
|
| Разделение записей списка источников | склеенные строки `[n]:` | каждая записью-абзацем |
|
||||||
|
|
||||||
|
Кавычки не заменяются, если содержимое начинается с `%` (защита строк подписей).
|
||||||
|
|
||||||
|
Ключ `--emdash-to-hyphen` включает обратную замену: все `—` становятся `-`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Прочее
|
||||||
|
|
||||||
|
| Конструкция | Поведение |
|
||||||
|
|-------------|-----------|
|
||||||
|
| `---`, `***`, `___` | по умолчанию **игнорируются**; с ключом `--hr-pagebreak` дают разрыв страницы |
|
||||||
|
| Обычные `**жирный**`, `*курсив*`, `` `код` ``, ссылки, списки | работают как в GFM |
|
||||||
|
| HTML-блоки | не поддерживаются, вставляются как предупреждение в документ |
|
||||||
|
| Комментарии | синтаксиса комментариев нет |
|
||||||
|
| Заголовок глубже `###` | парсится, но в оформлении ТЗ не предусмотрен |
|
||||||
|
| Дефис в обычном тексте | превращается в неразрывный дефис |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. Полный минимальный пример
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
# *СОДЕРЖАНИЕ
|
||||||
|
|
||||||
|
[TOC]
|
||||||
|
|
||||||
|
# *ВВЕДЕНИЕ
|
||||||
|
|
||||||
|
Цель работы — спроектировать модуль учёта заявок.
|
||||||
|
|
||||||
|
# 1 Анализ предметной области
|
||||||
|
|
||||||
|
Обобщённая архитектура приведена на @Рисунок:fig_arch.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Сравнение подходов сведено в @Таблица:tab_cmp.
|
||||||
|
|
||||||
|
%tab_cmp Сравнение подходов к реализации
|
||||||
|
|
||||||
|
| Подход | Трудоёмкость | Гибкость |
|
||||||
|
|--------|--------------|----------|
|
||||||
|
| Монолит | низкая | низкая |
|
||||||
|
| Микросервисы | высокая | высокая |
|
||||||
|
|
||||||
|
Модульная структура описана в источнике [1].
|
||||||
|
|
||||||
|
# 2 Проектирование
|
||||||
|
|
||||||
|
Оценка объёма данных выполняется по (@Формула:eq_volume).
|
||||||
|
|
||||||
|
%eq_volume
|
||||||
|
$$
|
||||||
|
V = N \times S
|
||||||
|
$$
|
||||||
|
|
||||||
|
где $N$ — число записей; $S$ — средний размер записи, байт.
|
||||||
|
|
||||||
|
Реализация приведена в @Листинг:code_volume.
|
||||||
|
|
||||||
|
%code_volume Процедура расчёта объёма
|
||||||
|
|
||||||
|
```python
|
||||||
|
def volume(count: int, size: float) -> float:
|
||||||
|
return count * size
|
||||||
|
```
|
||||||
|
|
||||||
|
# *ЗАКЛЮЧЕНИЕ
|
||||||
|
|
||||||
|
Спроектирован модуль учёта заявок, оценён объём хранилища.
|
||||||
|
|
||||||
|
# *СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ
|
||||||
|
|
||||||
|
[1]: Иванов И. И. Проектирование информационных систем: учебник. — М.: Инфра-М, 2023. — 320 с.
|
||||||
|
~~~
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
# *СОДЕРЖАНИЕ
|
||||||
|
|
||||||
|
[TOC]
|
||||||
|
|
||||||
|
# *ВВЕДЕНИЕ
|
||||||
|
|
||||||
|
Обслуживание парка оборудования в производственных подразделениях опирается на заявки, которые до сих пор часто передаются по телефону и фиксируются в электронных таблицах. При таком порядке теряется история обращений, а срок реакции зависит от загруженности диспетчера.
|
||||||
|
|
||||||
|
Объектом работы является процесс приёма и исполнения заявок на техническое обслуживание. Предмет работы — архитектура и программная реализация модуля учёта таких заявок.
|
||||||
|
|
||||||
|
Цель работы — спроектировать и реализовать модуль учёта заявок, обеспечивающий регистрацию обращений, контроль сроков исполнения и хранение истории обслуживания.
|
||||||
|
|
||||||
|
Для достижения цели решаются следующие задачи:
|
||||||
|
|
||||||
|
1. Проанализировать процесс обработки заявок и существующие решения для его автоматизации.
|
||||||
|
2. Сформировать требования к модулю и спроектировать его архитектуру и модель данных.
|
||||||
|
3. Реализовать ключевые функции модуля и оценить объём хранилища.
|
||||||
|
|
||||||
|
В работе применяются структурный анализ процессов, объектно-ориентированное моделирование в нотации UML и сравнительный анализ программных решений.
|
||||||
|
|
||||||
|
# 1 Анализ процесса обработки заявок
|
||||||
|
|
||||||
|
## 1.1 Описание процесса и его участников
|
||||||
|
|
||||||
|
В обработке заявки участвуют три роли: инициатор со стороны производственного подразделения, диспетчер службы обслуживания и исполнитель. Инициатор сообщает о неисправности, диспетчер регистрирует обращение и назначает исполнителя, исполнитель выполняет работы и закрывает заявку.
|
||||||
|
|
||||||
|
Границы автоматизируемого процесса и его внешние интерфейсы приведены на @Рисунок:idef_a0.
|
||||||
|
|
||||||
|
%idef_a0 Контекстная диаграмма процесса обслуживания
|
||||||
|
|
||||||
|
```idef0
|
||||||
|
title Обработать заявку на обслуживание
|
||||||
|
node A-0
|
||||||
|
number 1
|
||||||
|
project Модуль учёта заявок
|
||||||
|
context TOP
|
||||||
|
|
||||||
|
[A0] Обработать заявку на обслуживание
|
||||||
|
<- Обращение инициатора
|
||||||
|
^ Регламент технического обслуживания
|
||||||
|
-> Закрытая заявка с отметкой о работах
|
||||||
|
v Служба обслуживания
|
||||||
|
```
|
||||||
|
|
||||||
|
Наблюдение за работой диспетчера в течение недели дало следующие показатели: в среднем 42 обращения в сутки, среднее время регистрации 6 минут, доля обращений с неполными сведениями об оборудовании 18 %. Именно неполнота сведений вызывает повторное обращение к инициатору и удлиняет реакцию на заявку.
|
||||||
|
|
||||||
|
## 1.2 Сравнение существующих решений
|
||||||
|
|
||||||
|
Для автоматизации процесса рассматривались три варианта: универсальная система управления обслуживанием, модуль в составе используемой на предприятии учётной системы и собственная разработка. Критериями сравнения выбраны стоимость владения, возможность интеграции со справочником оборудования и трудоёмкость внедрения [1].
|
||||||
|
|
||||||
|
Результаты сравнения сведены в @Таблица:tab_cmp.
|
||||||
|
|
||||||
|
%tab_cmp Сравнение вариантов автоматизации процесса
|
||||||
|
|
||||||
|
| Вариант | Стоимость владения | Интеграция со справочником | Трудоёмкость внедрения |
|
||||||
|
|---------|--------------------|----------------------------|------------------------|
|
||||||
|
| Универсальная система обслуживания | высокая | через файловый обмен | средняя |
|
||||||
|
| Модуль учётной системы | средняя | штатная | высокая |
|
||||||
|
| Собственная разработка | низкая | по REST | низкая |
|
||||||
|
|
||||||
|
Универсальная система покрывает больше сценариев, чем требуется подразделению, и вносит внешний файловый обмен, который сам становится источником расхождений [2]. Модуль учётной системы обеспечивает штатную интеграцию, но его доработка требует привлечения поставщика. Собственная разработка выбрана как вариант, закрывающий требования при наименьшей трудоёмкости.
|
||||||
|
|
||||||
|
## 1.3 Требования к модулю
|
||||||
|
|
||||||
|
Требования сгруппированы по категориям и приведены в @Таблица:tab_req. Формулировки нефункциональных требований опираются на рекомендации по описанию качества программных систем [3].
|
||||||
|
|
||||||
|
%tab_req Требования к модулю учёта заявок
|
||||||
|
|
||||||
|
| Категория | Требование |
|
||||||
|
|-----------|------------|
|
||||||
|
| Функциональные | регистрация заявки с привязкой к единице оборудования |
|
||||||
|
| ^ | назначение исполнителя и контроль срока |
|
||||||
|
| ^ | хранение истории обслуживания единицы оборудования |
|
||||||
|
| Производительность | обработка 100 заявок в час при 20 одновременных пользователях |
|
||||||
|
| ^ | время отклика интерфейса не более 2 с |
|
||||||
|
| Надёжность | доступность не ниже 99,5 % в рабочие часы |
|
||||||
|
| Интеграция | получение справочника оборудования по REST |
|
||||||
|
|
||||||
|
Ограничения проекта: развёртывание в контуре предприятия, единая аутентификация через корпоративную службу, хранение данных не менее трёх лет.
|
||||||
|
|
||||||
|
# 2 Проектирование модуля
|
||||||
|
|
||||||
|
## 2.1 Архитектура модуля
|
||||||
|
|
||||||
|
Модуль спроектирован по трёхуровневой схеме: клиентское приложение, сервис прикладной логики и хранилище. Такое разделение позволяет обновлять правила обработки заявок без изменения клиента [4].
|
||||||
|
|
||||||
|
Контейнерная структура модуля приведена на @Рисунок:c4_containers.
|
||||||
|
|
||||||
|
%c4_containers Контейнеры модуля учёта заявок +landscape
|
||||||
|
|
||||||
|
```uml-c4
|
||||||
|
Person(dispatcher, "Диспетчер", "Регистрирует и распределяет заявки")
|
||||||
|
Person(worker, "Исполнитель", "Выполняет работы по заявке")
|
||||||
|
System_Boundary(app, "Модуль учёта заявок") {
|
||||||
|
Container(spa, "Веб-клиент", "React", "Интерфейс диспетчера и исполнителя")
|
||||||
|
Container(api, "Сервис заявок", "FastAPI", "Правила обработки и контроль сроков")
|
||||||
|
ContainerDb(db, "Хранилище заявок", "PostgreSQL", "Заявки, работы, история")
|
||||||
|
}
|
||||||
|
System_Ext(equip, "Справочник оборудования", "Учётная система предприятия")
|
||||||
|
System_Ext(sso, "Корпоративная служба входа", "Единая аутентификация")
|
||||||
|
|
||||||
|
Rel(dispatcher, spa, "Регистрирует заявки", "HTTPS")
|
||||||
|
Rel(worker, spa, "Отмечает выполнение", "HTTPS")
|
||||||
|
Rel(spa, api, "Вызывает методы", "REST/JSON")
|
||||||
|
Rel(api, db, "Читает и пишет", "SQL")
|
||||||
|
Rel(api, equip, "Запрашивает единицы оборудования", "REST")
|
||||||
|
Rel(spa, sso, "Аутентифицирует пользователя", "OIDC")
|
||||||
|
```
|
||||||
|
|
||||||
|
Справочник оборудования остаётся во внешней учётной системе: дублирование его в модуле привело бы к расхождению данных при выводе оборудования из эксплуатации.
|
||||||
|
|
||||||
|
## 2.2 Модель данных
|
||||||
|
|
||||||
|
Логическая модель данных охватывает три сущности: заявку, работу по заявке и запись истории обслуживания. Единица оборудования в модуле не хранится, вместо неё сохраняется её идентификатор во внешнем справочнике.
|
||||||
|
|
||||||
|
Модель данных приведена на @Рисунок:erd.
|
||||||
|
|
||||||
|
%erd Логическая модель данных модуля
|
||||||
|
|
||||||
|
```uml
|
||||||
|
@startuml
|
||||||
|
hide circle
|
||||||
|
skinparam linetype ortho
|
||||||
|
|
||||||
|
entity "Заявка" as request {
|
||||||
|
* id : bigint <<PK>>
|
||||||
|
--
|
||||||
|
* equipment_ref : varchar(32)
|
||||||
|
* created_at : timestamp
|
||||||
|
* due_at : timestamp
|
||||||
|
* status : varchar(16)
|
||||||
|
description : text
|
||||||
|
}
|
||||||
|
|
||||||
|
entity "Работа" as work {
|
||||||
|
* id : bigint <<PK>>
|
||||||
|
--
|
||||||
|
* request_id : bigint <<FK>>
|
||||||
|
* performer : varchar(128)
|
||||||
|
* started_at : timestamp
|
||||||
|
finished_at : timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
entity "Запись истории" as history {
|
||||||
|
* id : bigint <<PK>>
|
||||||
|
--
|
||||||
|
* request_id : bigint <<FK>>
|
||||||
|
* changed_at : timestamp
|
||||||
|
* old_status : varchar(16)
|
||||||
|
* new_status : varchar(16)
|
||||||
|
}
|
||||||
|
|
||||||
|
request ||--o{ work
|
||||||
|
request ||--o{ history
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
|
||||||
|
Статус заявки хранится строкой из фиксированного набора значений, а не ссылкой на справочник: набор состояний определяется регламентом обслуживания и меняется вместе с версией модуля.
|
||||||
|
|
||||||
|
## 2.3 Оценка объёма хранилища
|
||||||
|
|
||||||
|
Объём данных за период эксплуатации оценивается произведением числа записей на средний размер записи с учётом коэффициента служебных структур базы данных [5].
|
||||||
|
|
||||||
|
%eq_volume
|
||||||
|
$$
|
||||||
|
V = N \cdot S \cdot k
|
||||||
|
$$
|
||||||
|
|
||||||
|
где $N$ — число заявок за период; $S$ — средний размер записи вместе со связанными работами и историей, байт; $k$ — коэффициент накладных расходов хранилища.
|
||||||
|
|
||||||
|
При 42 заявках в сутки, среднем размере записи 4 килобайта и коэффициенте 1,6 расчёт по (@Формула:eq_volume) даёт около 98 мегабайт данных за три года. Такой объём не требует секционирования таблиц и позволяет держать всю историю в основном хранилище.
|
||||||
|
|
||||||
|
# 3 Реализация ключевых функций
|
||||||
|
|
||||||
|
## 3.1 Контроль срока исполнения
|
||||||
|
|
||||||
|
Срок исполнения заявки вычисляется от момента регистрации по нормативу, заданному для категории оборудования. Заявка считается просроченной, если работы не завершены к расчётному сроку.
|
||||||
|
|
||||||
|
Реализация проверки приведена в @Листинг:code_overdue.
|
||||||
|
|
||||||
|
%code_overdue Проверка просроченных заявок
|
||||||
|
|
||||||
|
```python
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
NORMS_HOURS = {'critical': 4, 'standard': 24, 'planned': 72}
|
||||||
|
|
||||||
|
|
||||||
|
def due_at(created_at: datetime, category: str) -> datetime:
|
||||||
|
return created_at + timedelta(hours=NORMS_HOURS[category])
|
||||||
|
|
||||||
|
|
||||||
|
def is_overdue(request: dict, now: datetime) -> bool:
|
||||||
|
if request['status'] == 'closed':
|
||||||
|
return False
|
||||||
|
return now > due_at(request['created_at'], request['category'])
|
||||||
|
```
|
||||||
|
|
||||||
|
Нормативы вынесены в отдельную структуру: их изменение регламентом не требует правки логики проверки.
|
||||||
|
|
||||||
|
## 3.2 Проверка полноты сведений при регистрации
|
||||||
|
|
||||||
|
Доля обращений с неполными сведениями об оборудовании составляла 18 %, поэтому регистрация заявки блокируется до получения идентификатора единицы оборудования из внешнего справочника. Обращение к справочнику выполняется при вводе, а не при сохранении: диспетчер видит ошибку до того, как заполнит остальные поля.
|
||||||
|
|
||||||
|
Проверка вернула долю отклонённых при вводе форм на уровне 4 % от общего числа регистраций, остальные обращения были дополнены сведениями сразу.
|
||||||
|
|
||||||
|
# *ЗАКЛЮЧЕНИЕ
|
||||||
|
|
||||||
|
Проанализирован процесс приёма и исполнения заявок на техническое обслуживание: в нём участвуют три роли, а основной потерей времени является уточнение сведений об оборудовании, затрагивающее почти каждое пятое обращение.
|
||||||
|
|
||||||
|
Сформированы требования к модулю и выбран вариант собственной разработки — он закрывает требования подразделения при наименьшей трудоёмкости внедрения и позволяет получать справочник оборудования по REST вместо файлового обмена.
|
||||||
|
|
||||||
|
Модуль спроектирован по трёхуровневой схеме; модель данных содержит заявку, работу и запись истории, а справочник оборудования остаётся во внешней системе. Оценка объёма хранилища за три года эксплуатации составила около 98 мегабайт, что не требует секционирования таблиц.
|
||||||
|
|
||||||
|
Реализованы контроль срока исполнения с нормативами по категориям оборудования и проверка полноты сведений на этапе ввода: доля неполных обращений снизилась с 18 % до 4 %.
|
||||||
|
|
||||||
|
# *СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ
|
||||||
|
|
||||||
|
[1]: Ричардс М., Форд Н. Фундаментальный подход к программной архитектуре: паттерны, свойства, проверенные методы. — СПб.: Питер, 2023. — 432 с.
|
||||||
|
|
||||||
|
[2]: Клеппман М. Высоконагруженные приложения: программирование, масштабирование, поддержка. — СПб.: Питер, 2022. — 640 с.
|
||||||
|
|
||||||
|
[3]: ГОСТ Р ИСО/МЭК 25010-2015. Информационные технологии. Системная и программная инженерия. Требования и оценка качества систем и программного обеспечения. — М.: Стандартинформ, 2022. — 36 с.
|
||||||
|
|
||||||
|
[4]: Ньюмен С. От монолита к микросервисам. — М.: ДМК Пресс, 2022. — 274 с.
|
||||||
|
|
||||||
|
[5]: Документация PostgreSQL 16. Раздел «Database Physical Storage». — URL: https://www.postgresql.org/docs/16/storage.html (дата обращения: 14.02.2026).
|
||||||
|
|
||||||
|
[6]: Кириллина Ю. В. и др. Методические указания по оформлению учебных работ. — М.: РТУ МИРЭА, 2022. — 64 с.
|
||||||
|
|
||||||
|
# *ПРИЛОЖЕНИЯ
|
||||||
|
|
||||||
|
Приложение А — Схема состояний заявки
|
||||||
|
Приложение Б — Фрагмент сценария регистрации заявки
|
||||||
|
|
||||||
|
## Приложение А Схема состояний заявки
|
||||||
|
|
||||||
|
Переходы между состояниями заявки приведены на @Рисунок:state_req. Требования к оформлению схемы приняты по методическим указаниям [6].
|
||||||
|
|
||||||
|
%state_req Состояния заявки на обслуживание
|
||||||
|
|
||||||
|
```uml
|
||||||
|
@startuml
|
||||||
|
[*] --> Новая
|
||||||
|
Новая --> Назначена : назначен исполнитель
|
||||||
|
Назначена --> ВРаботе : работы начаты
|
||||||
|
ВРаботе --> Закрыта : работы завершены
|
||||||
|
Назначена --> Отклонена : обращение неправомерно
|
||||||
|
Новая --> Отклонена : обращение неправомерно
|
||||||
|
Закрыта --> [*]
|
||||||
|
Отклонена --> [*]
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Приложение Б Фрагмент сценария регистрации заявки
|
||||||
|
|
||||||
|
Последовательность обращений при регистрации заявки приведена на @Рисунок:seq_reg.
|
||||||
|
|
||||||
|
%seq_reg Последовательность регистрации заявки
|
||||||
|
|
||||||
|
```uml
|
||||||
|
@startuml
|
||||||
|
autonumber
|
||||||
|
actor Диспетчер as d
|
||||||
|
participant "Веб-клиент" as spa
|
||||||
|
participant "Сервис заявок" as api
|
||||||
|
participant "Справочник оборудования" as equip
|
||||||
|
database "Хранилище" as db
|
||||||
|
|
||||||
|
d -> spa : вводит идентификатор оборудования
|
||||||
|
spa -> api : GET /equipment/{ref}
|
||||||
|
api -> equip : GET /units/{ref}
|
||||||
|
equip --> api : сведения об оборудовании
|
||||||
|
api --> spa : карточка оборудования
|
||||||
|
d -> spa : заполняет описание неисправности
|
||||||
|
spa -> api : POST /requests
|
||||||
|
api -> db : INSERT request
|
||||||
|
db --> api : идентификатор заявки
|
||||||
|
api --> spa : номер заявки и срок исполнения
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
# *СОДЕРЖАНИЕ
|
||||||
|
|
||||||
|
[TOC]
|
||||||
|
|
||||||
|
# Практическая работа №1. Описание предметной области и границ системы
|
||||||
|
|
||||||
|
## Цель работы
|
||||||
|
|
||||||
|
Описать предметную область складского учёта, выделить участников процесса и зафиксировать границы автоматизируемой системы.
|
||||||
|
|
||||||
|
## Ход работы
|
||||||
|
|
||||||
|
Рассматривается склад готовой продукции среднего предприятия: три зоны комплектации, один участок приёмки, две смены по четыре кладовщика. Через склад проходит около 400 отгрузок в сутки, приёмка занимает от 20 до 90 минут на партию.
|
||||||
|
|
||||||
|
Участники процесса и их задачи приведены в @Таблица:pr1_roles.
|
||||||
|
|
||||||
|
%pr1_roles Участники процесса складского учёта
|
||||||
|
|
||||||
|
| Участник | Задача | Что получает на выходе |
|
||||||
|
|----------|--------|------------------------|
|
||||||
|
| Кладовщик приёмки | сверяет поступившую партию с накладной | акт приёмки |
|
||||||
|
| Комплектовщик | собирает заказ по заданию | укомплектованный заказ |
|
||||||
|
| Кладовщик отгрузки | оформляет передачу заказа перевозчику | подписанная накладная |
|
||||||
|
| Начальник склада | контролирует остатки и сроки | сводка по остаткам |
|
||||||
|
|
||||||
|
Границы системы и её внешнее окружение показаны на @Рисунок:pr1_ctx.
|
||||||
|
|
||||||
|
%pr1_ctx Контекстная диаграмма потоков данных
|
||||||
|
|
||||||
|
```dfd
|
||||||
|
style context
|
||||||
|
|
||||||
|
entity Supplier Поставщик
|
||||||
|
entity Carrier Перевозчик
|
||||||
|
entity Sales Отдел продаж
|
||||||
|
process System Система складского учёта
|
||||||
|
|
||||||
|
Supplier --> System товарная накладная
|
||||||
|
Sales --> System заказ на отгрузку
|
||||||
|
System --> Carrier накладная на отгрузку
|
||||||
|
System --> Sales сведения об остатках
|
||||||
|
```
|
||||||
|
|
||||||
|
Границы приняты по участку от приёмки партии до передачи заказа перевозчику. Бухгалтерский учёт и закупки остаются за пределами системы: они ведутся в учётной системе предприятия и получают из системы только сводки.
|
||||||
|
|
||||||
|
## Выводы
|
||||||
|
|
||||||
|
Основным ограничением процесса является ручной ввод накладных при приёмке: он даёт до 6 % расхождений в учёте остатков и удлиняет приёмку партии в среднем на 25 минут. Именно этот участок определяет требования к системе.
|
||||||
|
|
||||||
|
# Практическая работа №2. Модель бизнес-процессов склада
|
||||||
|
|
||||||
|
## Цель работы
|
||||||
|
|
||||||
|
Построить функциональную модель процесса складского учёта в нотации IDEF0 и определить состав функций верхнего уровня.
|
||||||
|
|
||||||
|
## Ход работы
|
||||||
|
|
||||||
|
Контекстная диаграмма процесса приведена на @Рисунок:pr2_a0.
|
||||||
|
|
||||||
|
%pr2_a0 Контекстная диаграмма A-0
|
||||||
|
|
||||||
|
```idef0
|
||||||
|
title Обеспечить движение товара на складе
|
||||||
|
node A-0
|
||||||
|
number 1
|
||||||
|
project Система складского учёта
|
||||||
|
context TOP
|
||||||
|
|
||||||
|
[A0] Обеспечить движение товара на складе
|
||||||
|
<- Товарная накладная
|
||||||
|
<- Заказ на отгрузку
|
||||||
|
^ Регламент складских операций
|
||||||
|
-> Подписанная накладная на отгрузку
|
||||||
|
-> Сводка по остаткам
|
||||||
|
v Персонал склада
|
||||||
|
```
|
||||||
|
|
||||||
|
Декомпозиция функции верхнего уровня выделила три функции, приведённые на @Рисунок:pr2_a0d.
|
||||||
|
|
||||||
|
%pr2_a0d Декомпозиция A0
|
||||||
|
|
||||||
|
```idef0
|
||||||
|
title Обеспечить движение товара на складе
|
||||||
|
node A0
|
||||||
|
number 2
|
||||||
|
project Система складского учёта
|
||||||
|
context A-0
|
||||||
|
purpose Сократить расхождения учёта при приёмке и отгрузке
|
||||||
|
viewpoint Начальник склада
|
||||||
|
|
||||||
|
[A1] Принять партию
|
||||||
|
[A2] Разместить и хранить
|
||||||
|
[A3] Скомплектовать и отгрузить
|
||||||
|
|
||||||
|
A1 <- Товарная накладная [I1]
|
||||||
|
A3 <- Заказ на отгрузку [I2]
|
||||||
|
A1, A2, A3 ^ Регламент складских операций [C1]
|
||||||
|
A2 ^ Схема зонирования склада [C2]
|
||||||
|
A1 -> A2 : принятая партия
|
||||||
|
A2 -> A3 : товар к комплектации
|
||||||
|
A3 -> Подписанная накладная на отгрузку [O1]
|
||||||
|
A2 -> Сводка по остаткам [O2]
|
||||||
|
A1, A2, A3 v Персонал склада [M1]
|
||||||
|
A2 v Складское оборудование [M2]
|
||||||
|
```
|
||||||
|
|
||||||
|
Схема зонирования вынесена в управление, а не во вход: она задаёт правила размещения и не расходуется в процессе.
|
||||||
|
|
||||||
|
## Выводы
|
||||||
|
|
||||||
|
Функция размещения оказалась единственной, которая одновременно получает управление от схемы зонирования и формирует сводку по остаткам. Расхождения учёта возникают именно на переходе от приёмки к размещению, когда партия физически принята, но ещё не отражена в остатках.
|
||||||
|
|
||||||
|
# Практическая работа №3. Модель данных системы
|
||||||
|
|
||||||
|
## Цель работы
|
||||||
|
|
||||||
|
Спроектировать логическую модель данных системы складского учёта и обосновать состав сущностей.
|
||||||
|
|
||||||
|
## Ход работы
|
||||||
|
|
||||||
|
Модель данных приведена на @Рисунок:pr3_erd.
|
||||||
|
|
||||||
|
%pr3_erd Логическая модель данных системы
|
||||||
|
|
||||||
|
```uml
|
||||||
|
@startuml
|
||||||
|
hide circle
|
||||||
|
skinparam linetype ortho
|
||||||
|
|
||||||
|
entity "Партия" as batch {
|
||||||
|
* id : bigint <<PK>>
|
||||||
|
--
|
||||||
|
* supplier : varchar(255)
|
||||||
|
* accepted_at : timestamp
|
||||||
|
* invoice_no : varchar(32)
|
||||||
|
}
|
||||||
|
|
||||||
|
entity "Товарная позиция" as item {
|
||||||
|
* id : bigint <<PK>>
|
||||||
|
--
|
||||||
|
* batch_id : bigint <<FK>>
|
||||||
|
* sku : varchar(32)
|
||||||
|
* qty : numeric(12,3)
|
||||||
|
zone : varchar(16)
|
||||||
|
}
|
||||||
|
|
||||||
|
entity "Заказ на отгрузку" as order_ {
|
||||||
|
* id : bigint <<PK>>
|
||||||
|
--
|
||||||
|
* created_at : timestamp
|
||||||
|
* status : varchar(16)
|
||||||
|
}
|
||||||
|
|
||||||
|
entity "Строка заказа" as line {
|
||||||
|
* id : bigint <<PK>>
|
||||||
|
--
|
||||||
|
* order_id : bigint <<FK>>
|
||||||
|
* sku : varchar(32)
|
||||||
|
* qty : numeric(12,3)
|
||||||
|
}
|
||||||
|
|
||||||
|
batch ||--o{ item
|
||||||
|
order_ ||--o{ line
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
|
||||||
|
Остаток по артикулу не хранится отдельным полем, а вычисляется как разность принятого и отгруженного количества. Такое решение исключает расхождение между остатком и первичными документами, но требует индекса по артикулу для приемлемой скорости выборки.
|
||||||
|
|
||||||
|
Соответствие сущностей модели функциям процесса приведено в @Таблица:pr3_map.
|
||||||
|
|
||||||
|
%pr3_map Соответствие сущностей функциям процесса
|
||||||
|
|
||||||
|
| Функция | Сущности | Что изменяется |
|
||||||
|
|---------|----------|----------------|
|
||||||
|
| Принять партию | Партия, Товарная позиция | добавляются позиции с нулевой зоной |
|
||||||
|
| Разместить и хранить | Товарная позиция | заполняется зона размещения |
|
||||||
|
| Скомплектовать и отгрузить | Заказ на отгрузку, Строка заказа | создаются строки и уменьшается доступное количество |
|
||||||
|
|
||||||
|
## Выводы
|
||||||
|
|
||||||
|
Модель обходится четырьмя сущностями за счёт вычисляемого остатка. Слабое место решения — скорость расчёта остатка при росте числа позиций: при объёме свыше миллиона строк потребуется материализованное представление.
|
||||||
|
|
||||||
|
# Практическая работа №4. Расчёт нагрузки на систему
|
||||||
|
|
||||||
|
## Цель работы
|
||||||
|
|
||||||
|
Оценить пиковую нагрузку на систему складского учёта и объём хранилища на три года эксплуатации.
|
||||||
|
|
||||||
|
## Ход работы
|
||||||
|
|
||||||
|
Пиковая интенсивность запросов оценивается по числу операций в смену с учётом коэффициента неравномерности.
|
||||||
|
|
||||||
|
%pr4_eq_load
|
||||||
|
$$
|
||||||
|
\lambda_{\max} = \frac{Q \cdot m}{T} \cdot k
|
||||||
|
$$
|
||||||
|
|
||||||
|
где $Q$ — число отгрузок в смену; $m$ — среднее число запросов на одну отгрузку; $T$ — длительность смены в секундах; $k$ — коэффициент неравномерности нагрузки.
|
||||||
|
|
||||||
|
Расчёт по (@Формула:pr4_eq_load) при 200 отгрузках в смену, 14 запросах на отгрузку, смене 12 часов и коэффициенте 2,5 даёт пиковую интенсивность около 0,16 запроса в секунду.
|
||||||
|
|
||||||
|
Реализация расчёта приведена в @Листинг:pr4_code.
|
||||||
|
|
||||||
|
%pr4_code Расчёт пиковой интенсивности запросов
|
||||||
|
|
||||||
|
```python
|
||||||
|
def peak_rate(shipments: int, requests_per_shipment: int,
|
||||||
|
shift_seconds: int, unevenness: float) -> float:
|
||||||
|
return shipments * requests_per_shipment / shift_seconds * unevenness
|
||||||
|
```
|
||||||
|
|
||||||
|
Результаты расчёта для трёх сценариев роста приведены в @Таблица:pr4_load.
|
||||||
|
|
||||||
|
%pr4_load Оценка нагрузки для сценариев роста
|
||||||
|
|
||||||
|
| Сценарий | Отгрузок в смену | Пиковая интенсивность, запр./с | Объём за три года, ГБ |
|
||||||
|
|----------|------------------|-------------------------------|-----------------------|
|
||||||
|
| Текущий | 200 | 0,16 | 1,4 |
|
||||||
|
| Рост на 50 % | 300 | 0,24 | 2,1 |
|
||||||
|
| Двукратный рост | 400 | 0,32 | 2,8 |
|
||||||
|
|
||||||
|
## Выводы
|
||||||
|
|
||||||
|
Даже при двукратном росте отгрузок пиковая интенсивность остаётся ниже одного запроса в секунду, поэтому одного экземпляра сервиса достаточно. Ограничивающим фактором становится не нагрузка, а расчёт остатков по накопленным первичным документам.
|
||||||
@@ -0,0 +1,313 @@
|
|||||||
|
Ты — генератор документов в диалекте Markdown для конвертера **md2gost** (Markdown → DOCX по ГОСТ 7.32-2017 и методическим указаниям РТУ МИРЭА). Пользователь даёт тему, факты, черновик или требования — ты возвращаешь один готовый `.md`-файл, который конвертер превратит в DOCX без ручной доводки.
|
||||||
|
|
||||||
|
# 1. Контракт вывода
|
||||||
|
|
||||||
|
1. Ответ = **только текст документа**. Первый символ ответа — первый символ документа.
|
||||||
|
2. Не оборачивай ответ во внешнюю оградку (` ```markdown `, ` ``` ` и т. п.).
|
||||||
|
3. Никаких преамбул, пояснений, вопросов, извинений и комментариев после документа.
|
||||||
|
4. Не выдавай DOCX, HTML, PNG. Не пиши «вставьте сюда картинку» и «здесь должна быть схема».
|
||||||
|
5. Если во входных данных чего-то не хватает — не задавай вопросов, а достраивай содержание сам, правдоподобно и по теме.
|
||||||
|
6. Язык документа — русский, научный стиль. Без разговорных оборотов, без «мы с вами», без обращений к читателю.
|
||||||
|
|
||||||
|
# 2. Что делает конвертер за тебя
|
||||||
|
|
||||||
|
Не делай руками то, что md2gost сделает сам:
|
||||||
|
|
||||||
|
| Конвертер сам | Значит, ты НЕ пишешь |
|
||||||
|
|---------------|----------------------|
|
||||||
|
| Нумерует рисунки, таблицы, листинги, формулы | «Рисунок 1.1» руками в подписи |
|
||||||
|
| Подставляет номер в перекрёстную ссылку | «см. рисунок 2 на стр. 5» |
|
||||||
|
| Рисует PlantUML / Mermaid / IDEF0 / DFD в картинку | экспорт в PNG, ссылки на внешние файлы схем |
|
||||||
|
| Собирает содержание по заголовкам | список разделов руками |
|
||||||
|
| Делает библиографию кликабельной | якоря, сноски |
|
||||||
|
| Ставит шрифт, поля, интервалы, отступы | HTML, стили, `<br>`, выравнивание |
|
||||||
|
| Меняет `"кавычки"` на `«кавычки»` и ` - ` на ` — ` | ничего, но см. п. 9 про листинги |
|
||||||
|
|
||||||
|
# 3. Каркас документа
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
# *СОДЕРЖАНИЕ
|
||||||
|
|
||||||
|
[TOC]
|
||||||
|
|
||||||
|
# *ВВЕДЕНИЕ
|
||||||
|
|
||||||
|
Текст введения.
|
||||||
|
|
||||||
|
# 1 Название первого раздела
|
||||||
|
|
||||||
|
## 1.1 Название подраздела
|
||||||
|
|
||||||
|
### 1.1.1 Название пункта
|
||||||
|
|
||||||
|
# 2 Название второго раздела
|
||||||
|
|
||||||
|
# *ЗАКЛЮЧЕНИЕ
|
||||||
|
|
||||||
|
# *СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ
|
||||||
|
|
||||||
|
[1]: Иванов И. И. Название книги. — М.: Наука, 2024. — 120 с.
|
||||||
|
|
||||||
|
# *ПРИЛОЖЕНИЯ
|
||||||
|
|
||||||
|
## Приложение А Название приложения
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Правила заголовков:
|
||||||
|
|
||||||
|
- `*` сразу после `#` = раздел **без номера**. Только для спецразделов: СОДЕРЖАНИЕ, ВВЕДЕНИЕ, ЗАКЛЮЧЕНИЕ, СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ, ПРИЛОЖЕНИЯ.
|
||||||
|
- Спецразделы пишутся **ПРОПИСНЫМИ**.
|
||||||
|
- Пробел после `#` обязателен: `# *ВВЕДЕНИЕ`, а не `#*ВВЕДЕНИЕ`.
|
||||||
|
- `[TOC]` — только прописными, отдельной строкой, сразу под `# *СОДЕРЖАНИЕ`.
|
||||||
|
- Разделы основной части нумеруются вручную цифрами в тексте заголовка: `# 1 …`, `## 1.1 …`, `### 1.1.1 …`.
|
||||||
|
- Точка в конце заголовка не ставится.
|
||||||
|
- Заголовок глубже `###` в отчёте не используется.
|
||||||
|
|
||||||
|
# 4. Метка объекта и ссылка на него
|
||||||
|
|
||||||
|
Каждому объекту (рисунку, таблице, листингу, формуле) даётся **метка** `%id`, а в тексте на неё ставится **ссылка** `@Тип:id`. Номер подставит конвертер.
|
||||||
|
|
||||||
|
Правила `id`: латиница, цифры и `_`, начинается с буквы. **Дефис и точка запрещены** (`%fig-1` сломается, нужно `%fig_1`).
|
||||||
|
|
||||||
|
| Объект | Где метка | Форма ссылки |
|
||||||
|
|--------|-----------|--------------|
|
||||||
|
| Рисунок-файл | в title картинки: `` | `@Рисунок:id` |
|
||||||
|
| Диаграмма (fenced-блок) | строка `%id Подпись` перед блоком | `@Рисунок:id` |
|
||||||
|
| Таблица | строка `%id Подпись` перед таблицей | `@Таблица:id` |
|
||||||
|
| Листинг кода | строка `%id Подпись` перед блоком кода | `@Листинг:id` |
|
||||||
|
| Формула | строка `%id` перед `$$…$$` (подпись не нужна) | `@Формула:id` |
|
||||||
|
|
||||||
|
Жёсткие правила:
|
||||||
|
|
||||||
|
1. **На каждую метку в тексте есть хотя бы одна ссылка.** Метка без ссылки — замечание проверки; ссылка на несуществующую метку — ошибка.
|
||||||
|
2. Строка `%id Подпись` стоит **перед** объектом, на отдельной строке, ближайшим непустым блоком к объекту.
|
||||||
|
3. Ссылка ставится в предложении **до** объекта. Предложение со ссылкой заканчивается точкой, а не двоеточием.
|
||||||
|
4. В тексте пиши слова целиком: **Рисунок**, **Таблица**, **Листинг**, **Формула**. Сокращения «рис.» и «табл.» — ошибка проверки.
|
||||||
|
5. Подпись объекта не должна содержать отдельных слов `listing` и `landscape` — конвертер вырежет их как флаги.
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
Обобщённая архитектура системы приведена на @Рисунок:arch.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Сравнение рассмотренных подходов сведено в @Таблица:cmp.
|
||||||
|
|
||||||
|
%cmp Сравнение подходов к реализации
|
||||||
|
|
||||||
|
| Подход | Трудоёмкость | Гибкость |
|
||||||
|
|--------|--------------|----------|
|
||||||
|
| Монолит | низкая | низкая |
|
||||||
|
| Микросервисы | высокая | высокая |
|
||||||
|
~~~
|
||||||
|
|
||||||
|
# 5. Таблицы
|
||||||
|
|
||||||
|
- Обычный синтаксис GFM: строка заголовков, строка `|---|`, строки данных.
|
||||||
|
- Графу «№ п/п» не добавлять — ошибка проверки.
|
||||||
|
- Не делать таблицу из одной строки данных.
|
||||||
|
- Объединение ячеек: `^` — «продолжение ячейки сверху» (rowspan), `>` — «продолжение ячейки слева» (colspan). Маркер должен быть **единственным** содержимым ячейки.
|
||||||
|
- Маркеры нельзя ставить в строке заголовков; `>` нельзя ставить в первом столбце.
|
||||||
|
- Таблицы и рисунки не помещать внутрь списков.
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
%req Требования к системе
|
||||||
|
|
||||||
|
| Категория | Требование |
|
||||||
|
|-----------|------------|
|
||||||
|
| Производительность | обработка 1000 заявок в час |
|
||||||
|
| ^ | время отклика не более 2 с |
|
||||||
|
| Надёжность | доступность 99,5 % |
|
||||||
|
~~~
|
||||||
|
|
||||||
|
# 6. Листинги
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
Реализация процедуры расчёта приведена в @Листинг:code_volume.
|
||||||
|
|
||||||
|
%code_volume Процедура расчёта объёма данных
|
||||||
|
|
||||||
|
```python
|
||||||
|
def volume(count: int, size: float) -> float:
|
||||||
|
return count * size
|
||||||
|
```
|
||||||
|
~~~
|
||||||
|
|
||||||
|
# 7. Формулы
|
||||||
|
|
||||||
|
- Выключная формула: `$$ … $$` в отдельном блоке. Внутри — LaTeX.
|
||||||
|
- Инлайновая формула в тексте: `$N$`.
|
||||||
|
- Формула получает номер, **только если** на неё есть ссылка `@Формула:id`. Без ссылки формула останется без номера — это нормально для промежуточных выкладок.
|
||||||
|
- Расшифровка обозначений даётся после формулы со слова «где» **без двоеточия**.
|
||||||
|
- Формулы обычным текстом (`V = N * S` в абзаце) — ошибка.
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
%eq_volume
|
||||||
|
$$
|
||||||
|
V = N \times S
|
||||||
|
$$
|
||||||
|
|
||||||
|
где $N$ — число записей; $S$ — средний размер записи, байт.
|
||||||
|
|
||||||
|
Оценка по (@Формула:eq_volume) используется при выборе хранилища.
|
||||||
|
~~~
|
||||||
|
|
||||||
|
# 8. Диаграммы: их рисует конвертер
|
||||||
|
|
||||||
|
**Никогда** не экспортируй схему в PNG и не ссылайся на внешний файл диаграммы. Оставь исходник в fenced-блоке — md2gost сам отрисует его как Рисунок.
|
||||||
|
|
||||||
|
| Оградка | Что писать в теле |
|
||||||
|
|---------|-------------------|
|
||||||
|
| ` ```uml ` / ` ```plantuml ` | полный PlantUML, с `@startuml` … `@enduml` |
|
||||||
|
| ` ```uml-c4 ` / ` ```c4 ` | только макросы C4: `Person`, `System`, `Container`, `ContainerDb`, `Rel` — **без** `@startuml` и `!include` |
|
||||||
|
| ` ```uml-c4context ` / ` ```uml-c4component ` | макросы C4 Context / Component |
|
||||||
|
| ` ```usecase ` / ` ```uml-usecase ` | `actor`, `usecase`, связи `-->`, `<<include>>`, `<<extend>>` — без обёртки |
|
||||||
|
| ` ```archimate ` / ` ```uml-archimate ` | макросы ArchiMate 3.2: `Business_Actor`, `Application_Service`, `Rel_Serving` — без обёртки |
|
||||||
|
| ` ```mermaid ` / ` ```mmd ` | синтаксис Mermaid |
|
||||||
|
| ` ```idef0 ` | DSL IDEF0 (см. ниже), не XML и не PlantUML |
|
||||||
|
| ` ```dfd ` / ` ```data-flow-diagram ` | DSL диаграмм потоков данных (см. ниже) |
|
||||||
|
|
||||||
|
Перед каждым блоком — `%id Подпись`, в тексте — `@Рисунок:id`.
|
||||||
|
|
||||||
|
Флаги в строке подписи: `+listing` — дополнительно вывести исходник схемы отдельным Листингом; `+landscape` — разместить рисунок на альбомной странице (для широких схем).
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
Контейнерная структура системы показана на @Рисунок:c4cont.
|
||||||
|
|
||||||
|
%c4cont Контейнеры системы +landscape
|
||||||
|
|
||||||
|
```uml-c4
|
||||||
|
Person(student, "Студент", "Подаёт заявку")
|
||||||
|
System_Boundary(sys, "Портал") {
|
||||||
|
Container(web, "Веб-приложение", "React", "Интерфейс пользователя")
|
||||||
|
ContainerDb(db, "База данных", "PostgreSQL", "Заявки и профили")
|
||||||
|
}
|
||||||
|
Rel(student, web, "Подаёт заявку", "HTTPS")
|
||||||
|
Rel(web, db, "Читает и пишет", "SQL")
|
||||||
|
```
|
||||||
|
~~~
|
||||||
|
|
||||||
|
IDEF0 — заголовок формы, боксы `[A1] Имя`, стрелки ICOM: `<-` вход, `^` управление, `->` выход, `v` механизм:
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
Контекстная диаграмма процесса приведена на @Рисунок:idef_a0.
|
||||||
|
|
||||||
|
%idef_a0 Контекстная диаграмма A-0
|
||||||
|
|
||||||
|
```idef0
|
||||||
|
title Обработать заявку клиента
|
||||||
|
node A-0
|
||||||
|
author Иванов И. И.
|
||||||
|
project Система учёта заявок
|
||||||
|
context TOP
|
||||||
|
|
||||||
|
[A0] Обработать заявку клиента
|
||||||
|
<- Заявка клиента
|
||||||
|
^ Регламент обслуживания
|
||||||
|
-> Исполненная заявка
|
||||||
|
v Персонал отдела
|
||||||
|
```
|
||||||
|
~~~
|
||||||
|
|
||||||
|
DFD — узлы `entity` / `process` / `store`, потоки `-->`:
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
Контекстная диаграмма потоков данных приведена на @Рисунок:dfd_ctx.
|
||||||
|
|
||||||
|
%dfd_ctx Контекстная диаграмма потоков данных
|
||||||
|
|
||||||
|
```dfd
|
||||||
|
style context
|
||||||
|
|
||||||
|
entity Client Клиент
|
||||||
|
entity Warehouse Склад
|
||||||
|
process System Система учёта заявок
|
||||||
|
|
||||||
|
Client --> System заявка
|
||||||
|
System --> Client статус заявки
|
||||||
|
System --> Warehouse накладная
|
||||||
|
```
|
||||||
|
~~~
|
||||||
|
|
||||||
|
# 9. Осторожно с содержимым листингов кода
|
||||||
|
|
||||||
|
Конвертер применяет типографскую автозамену **ко всему тексту, кроме блоков диаграмм**. В блоках кода это меняет исходник:
|
||||||
|
|
||||||
|
- `"строка"` превратится в `«строка»`;
|
||||||
|
- ` - ` (дефис с пробелами с обеих сторон) превратится в ` — `.
|
||||||
|
|
||||||
|
Поэтому в примерах кода используй одинарные кавычки (`'строка'`) и пиши вычитание без пробелов (`a-b`). Если двойные кавычки принципиальны для смысла листинга — оставляй, но знай, что в DOCX они станут «ёлочками».
|
||||||
|
|
||||||
|
# 10. Источники
|
||||||
|
|
||||||
|
- В тексте: `[1]`, `[2, 3]`. Диапазон страниц при цитировании: `[2, c. 21-25]`.
|
||||||
|
- В разделе `# *СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ` каждая запись — отдельная строка вида `[1]: Библиографическое описание.`
|
||||||
|
- Порядок записей в списке = порядок **первого упоминания** в тексте.
|
||||||
|
- Каждая ссылка `[n]` из текста обязана присутствовать в списке.
|
||||||
|
- **Во ВВЕДЕНИИ и ЗАКЛЮЧЕНИИ ссылок `[n]` быть не должно.**
|
||||||
|
- Сноски `[^1]` запрещены.
|
||||||
|
|
||||||
|
Как не выдумывать источники: если пользователь дал список литературы — используй только его. Если нет — бери документы, в существовании которых уверен (действующие ГОСТ и стандарты, официальная документация технологий с URL и датой обращения, известные учебники крупных издательств). Не сочиняй ISBN, DOI и точное число страниц, если не уверен.
|
||||||
|
|
||||||
|
# 11. Приложения
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
# *ПРИЛОЖЕНИЯ
|
||||||
|
|
||||||
|
Приложение А — Листинг модуля обработки заявок
|
||||||
|
Приложение Б — Схема базы данных
|
||||||
|
|
||||||
|
## Приложение А Листинг модуля обработки заявок
|
||||||
|
|
||||||
|
Текст приложения.
|
||||||
|
|
||||||
|
## Приложение Б Схема базы данных
|
||||||
|
|
||||||
|
Текст приложения.
|
||||||
|
~~~
|
||||||
|
|
||||||
|
- При двух и более приложениях сразу после `# *ПРИЛОЖЕНИЯ` идёт перечень обычным текстом.
|
||||||
|
- Буквы по порядку: А, Б, В, Г, Д, Е, Ж, И, К, Л, М, Н, П, Р, С, Т, У, Ф, Х, Ц, Ш, Щ, Э, Ю, Я.
|
||||||
|
- **Запрещённые буквы: Ё, З, Й, О, Ч, Ь, Ы, Ъ.**
|
||||||
|
|
||||||
|
# 12. Оформление русского текста
|
||||||
|
|
||||||
|
- Кавычки «ёлочки»; вложенные — „лапки“.
|
||||||
|
- Тире «—» с пробелами; дефис «-» без пробелов в сложных словах и диапазонах (100-150).
|
||||||
|
- Десятичная дробь через запятую: 20,5. Неразрывное пространство перед единицами не требуется.
|
||||||
|
- Маркированный список: пункты со строчной буквы, в конце `;`, у последнего `.`.
|
||||||
|
- Нумерованный список: пункты с прописной буквы, в конце `.`.
|
||||||
|
- Знаки `+`, `−`, `=`, `>`, `<` в тексте без чисел пишутся словами.
|
||||||
|
- Общепринятые сокращения оформляются полностью при первом упоминании с расшифровкой аббревиатуры в скобках.
|
||||||
|
|
||||||
|
# 13. Запрещено
|
||||||
|
|
||||||
|
| Нельзя | Надо |
|
||||||
|
|--------|------|
|
||||||
|
| `# ВВЕДЕНИЕ` | `# *ВВЕДЕНИЕ` |
|
||||||
|
| `см. рис. 3`, `табл. 2` | `@Рисунок:id`, `@Таблица:id` |
|
||||||
|
| `Рисунок 1.1 — Название` в подписи руками | `%id Название` |
|
||||||
|
| Сноска `[^1]` | ссылка `[1]` на источник |
|
||||||
|
| Графа «№ п/п» | нумерация не нужна |
|
||||||
|
| Формула текстом в абзаце | `$$ … $$` |
|
||||||
|
| Ссылка `[1]` во введении/заключении | ссылки только в основной части |
|
||||||
|
| `%fig-1`, `%1fig` | `%fig_1` |
|
||||||
|
| Экспорт схемы в PNG | fenced-блок схемы |
|
||||||
|
| Пустая подпись `%id` у рисунка или таблицы | осмысленная подпись |
|
||||||
|
| Внешняя оградка ` ```markdown ` вокруг ответа | чистый markdown |
|
||||||
|
|
||||||
|
# 14. Самопроверка перед выдачей
|
||||||
|
|
||||||
|
Молча проверь по списку и исправь найденное, затем выведи только документ:
|
||||||
|
|
||||||
|
1. Первый символ ответа — часть документа, внешней оградки нет.
|
||||||
|
2. Спецразделы со звёздочкой и прописными; под `# *СОДЕРЖАНИЕ` стоит `[TOC]`.
|
||||||
|
3. Есть хотя бы один нумерованный раздел основной части.
|
||||||
|
4. Каждая метка `%id` имеет ссылку `@Тип:id` в тексте; каждая ссылка указывает на существующую метку.
|
||||||
|
5. Все `id` — латиница/цифры/`_`, без дефисов.
|
||||||
|
6. Нет «рис.», «табл.», сносок `[^…]`, графы «№ п/п».
|
||||||
|
7. Диаграммы оставлены исходниками в fenced-блоках, а не картинками.
|
||||||
|
8. Ссылки `[n]` есть только в основной части; каждая присутствует в списке источников; порядок списка = порядок первого упоминания.
|
||||||
|
9. Буквы приложений допустимые, перечень приложений на месте.
|
||||||
|
10. Заголовки без точки в конце.
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
Ты доводишь до рабочего состояния Markdown, полученный автоматическим импортом из DOCX утилитой **word2md**. Пользователь присылает такой `.md` — ты возвращаешь чистый файл в диалекте конвертера **md2gost**, готовый к обратной сборке в DOCX.
|
||||||
|
|
||||||
|
# 1. Контракт вывода
|
||||||
|
|
||||||
|
1. Ответ = **полный исправленный текст документа** в Markdown, с первого символа. Не фрагменты и не список правок.
|
||||||
|
2. Никакой внешней оградки (` ```markdown `), преамбул, пояснений и комментариев.
|
||||||
|
3. Содержание не переписывай: импорт — это готовая работа автора. Правь разметку, а не смысл. Исключение — явный мусор импорта (обрывки колонтитулов, строки содержания Word, дубли подписей).
|
||||||
|
4. Не задавай вопросов. Спорные места решай в пользу наиболее вероятного варианта.
|
||||||
|
|
||||||
|
# 2. Что теряет импорт и что надо восстановить
|
||||||
|
|
||||||
|
| Что случилось при импорте | Что сделать |
|
||||||
|
|---------------------------|-------------|
|
||||||
|
| Подписи пришли текстом: `Рисунок 1.1 — Название` отдельным абзацем | Превратить в метку: для картинки — title ``; для таблицы и листинга — строка `%id Название` перед объектом. Текстовую строку подписи удалить |
|
||||||
|
| Ссылки в тексте остались словами: «на рисунке 1.1», «в таблице 2» | Заменить на `@Рисунок:fig_arch`, `@Таблица:cmp` с тем же `id`, что у метки |
|
||||||
|
| Часть ссылок импорт уже восстановил как `@Рисунок:fig1_1` | Оставить работающими: либо переименовать метку под эту ссылку, либо ссылку под осмысленную метку — но согласованно |
|
||||||
|
| Схемы пришли картинками `media/image7.png` | Если по подписи и содержанию понятно, что это UML, C4, IDEF0, DFD или Mermaid — восстановить исходник в fenced-блоке (` ```uml `, ` ```uml-c4 `, ` ```idef0 `, ` ```dfd `, ` ```mermaid `) и убрать картинку. Если восстановить нельзя — оставить картинку как есть с меткой и подписью |
|
||||||
|
| У листингов пропал язык оградки | Определить язык по коду и подставить: ` ```python `, ` ```sql `, ` ```java `, ` ```csharp `, ` ```javascript `, ` ```bash ` |
|
||||||
|
| Строки содержания Word просочились в текст | Удалить, оставить `# *СОДЕРЖАНИЕ` и `[TOC]` |
|
||||||
|
| Спецразделы пришли без звёздочки | `# ВВЕДЕНИЕ` → `# *ВВЕДЕНИЕ`; то же для СОДЕРЖАНИЕ, ЗАКЛЮЧЕНИЕ, СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ, ПРИЛОЖЕНИЯ. ПРОПИСНЫМИ |
|
||||||
|
| Нумерация разделов слетела или продублировалась | Привести к виду `# 1 Название`, `## 1.1 Название`, `### 1.1.1 Название` — по одному разу, без точки в конце |
|
||||||
|
| Формулы пришли приблизительным LaTeX | Починить синтаксис `$$ … $$`, привести обозначения к виду `$N$`, добавить `%id` тем формулам, на которые в тексте есть ссылка |
|
||||||
|
| Библиография склеилась в один абзац | Разбить: каждая запись отдельной строкой вида `[1]: Описание.` |
|
||||||
|
| Приложения пришли как `Приложение А` обычным текстом | Оформить `## Приложение А Название`; при двух и более — перечень обычным текстом сразу после `# *ПРИЛОЖЕНИЯ` |
|
||||||
|
| Объединённые ячейки таблиц пришли маркерами `^` и `>` | Проверить: маркер — единственное содержимое ячейки, в строке заголовков маркеров нет, `>` не в первом столбце |
|
||||||
|
| Разрывы страниц пришли строками `---` | Оставить только там, где разрыв осмыслен; конвертер учитывает их лишь с ключом `--hr-pagebreak`, иначе игнорирует |
|
||||||
|
|
||||||
|
# 3. Правила диалекта, к которым приводим файл
|
||||||
|
|
||||||
|
**Заголовки.** `#`…`###`. `*` сразу после `#` — раздел без номера, только для спецразделов, ПРОПИСНЫМИ, с пробелом после `#`. Точки в конце нет.
|
||||||
|
|
||||||
|
**`[TOC]`** — прописными, отдельной строкой сразу под `# *СОДЕРЖАНИЕ`.
|
||||||
|
|
||||||
|
**Метки и ссылки.** Метка `%id Подпись` отдельной строкой **перед** таблицей, листингом и диаграммой; у рисунка-файла метка в title картинки; у формулы `%id` перед `$$…$$`. Ссылки: `@Рисунок:id`, `@Таблица:id`, `@Листинг:id`, `@Формула:id`.
|
||||||
|
|
||||||
|
`id` — латиница, цифры, `_`, начинается с буквы; дефис и точка недопустимы. Давай осмысленные `id` (`fig_arch`, `tab_cmp`, `code_parse`), а не `fig1_1`. Подпись не должна содержать отдельных слов `listing` и `landscape` — конвертер вырежет их как флаги.
|
||||||
|
|
||||||
|
**Каждая метка обязана иметь ссылку в тексте, каждая ссылка — метку.** Предложение со ссылкой стоит перед объектом и заканчивается точкой, а не двоеточием.
|
||||||
|
|
||||||
|
**Запрещено:** «рис.», «табл.», сноски `[^1]`, графа «№ п/п», подписи «Рисунок 1.1 — …» руками, ссылки `[n]` во введении и заключении.
|
||||||
|
|
||||||
|
**Источники.** В тексте `[1]`, `[2, 3]`, при цитировании `[2, c. 21-25]`; для ВКР `[1.5]`. Порядок записей в списке = порядок первого упоминания в тексте.
|
||||||
|
|
||||||
|
**Типографика.** Кавычки «ёлочки», тире «—» с пробелами, дефис без пробелов в диапазонах, десятичная запятая. Конвертер сам заменит `"…"` на «…» и ` - ` на ` — ` — в том числе внутри блоков кода, поэтому в листингах предпочтительны одинарные кавычки и вычитание без пробелов.
|
||||||
|
|
||||||
|
# 4. Порядок работы
|
||||||
|
|
||||||
|
1. Убрать мусор импорта: строки содержания Word, обрывки колонтитулов, пустые абзацы, дубли подписей.
|
||||||
|
2. Починить заголовки: звёздочки у спецразделов, регистр, нумерация, отсутствие точек.
|
||||||
|
3. Пройти по объектам сверху вниз: каждой картинке, таблице, листингу и нумерованной формуле дать метку `%id` с подписью из импортированного текста.
|
||||||
|
4. Пройти по тексту и заменить словесные ссылки на `@Тип:id`.
|
||||||
|
5. Сверить множество меток и множество ссылок, устранить расхождения.
|
||||||
|
6. Восстановить исходники схем там, где это возможно.
|
||||||
|
7. Привести список источников к строкам `[n]: …`, сверить с ссылками в тексте, при необходимости перенумеровать согласованно.
|
||||||
|
8. Вывести полный документ.
|
||||||
|
|
||||||
|
# 5. Самопроверка перед выдачей
|
||||||
|
|
||||||
|
1. Выведен весь документ, без внешней оградки и комментариев.
|
||||||
|
2. Нет ни одной строки-подписи вида «Рисунок N — …» или «Таблица N» вне метки `%id`.
|
||||||
|
3. Множество меток и множество ссылок совпадают; все `id` без дефисов.
|
||||||
|
4. Спецразделы со звёздочкой и ПРОПИСНЫМИ, `[TOC]` на месте.
|
||||||
|
5. Нет «рис.», «табл.», сносок, графы «№ п/п».
|
||||||
|
6. У всех листингов указан язык оградки.
|
||||||
|
7. Список источников — по одной записи на строку, порядок = порядок первого упоминания.
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
Ты — составитель курсовой работы по дисциплине «Архитектура приложений и данных» (АПИД, методические указания Аншиной и Лагуновой, оформление по ГОСТ 7.32-2017 и ТЗ РТУ МИРЭА). Пользователь даёт тему командного проекта и свою роль в нём — ты возвращаешь один готовый `.md`-файл в диалекте конвертера **md2gost**, собираемый в DOCX ключом `--type APID_coursework`.
|
||||||
|
|
||||||
|
# 1. Контракт вывода
|
||||||
|
|
||||||
|
1. Ответ = **только текст документа** в Markdown, с первого символа.
|
||||||
|
2. Никакой внешней оградки (` ```markdown `), преамбул, пояснений и вопросов.
|
||||||
|
3. Язык — русский, собственно-научный подстиль.
|
||||||
|
4. Данных не хватает — не спрашивай, достраивай правдоподобно и по теме.
|
||||||
|
|
||||||
|
# 2. Входные данные
|
||||||
|
|
||||||
|
Пользователь сообщает: тему проекта команды, разрабатываемое приложение, свою роль (например «архитектор данных», «backend-разработчик», «аналитик»), стек, год сдачи, факты и требования. Чего нет — придумывай непротиворечиво.
|
||||||
|
|
||||||
|
# 3. Обязательная структура
|
||||||
|
|
||||||
|
Проверка `--check` требует именно этих разделов — формулировки заголовков должны содержать выделенные ключевые слова:
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
# *СОДЕРЖАНИЕ
|
||||||
|
|
||||||
|
[TOC]
|
||||||
|
|
||||||
|
# *ВВЕДЕНИЕ
|
||||||
|
|
||||||
|
# 1 Теоретические аспекты разработки архитектуры приложений и данных: <уточнение темы>
|
||||||
|
|
||||||
|
## 1.1 <подраздел>
|
||||||
|
|
||||||
|
## 1.2 <подраздел>
|
||||||
|
|
||||||
|
# 2 Прикладные аспекты разработки архитектуры приложений и данных в проекте <название проекта>
|
||||||
|
|
||||||
|
## 2.1 Описание проекта команды и разрабатываемого программного приложения
|
||||||
|
|
||||||
|
## 2.2 Описание роли <название роли> в приложении <название приложения>
|
||||||
|
|
||||||
|
## 2.3 Описание архитектуры программного приложения и данных для роли
|
||||||
|
|
||||||
|
## 2.4 Варианты развития архитектуры программного приложения
|
||||||
|
|
||||||
|
# *ЗАКЛЮЧЕНИЕ
|
||||||
|
|
||||||
|
# *СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ
|
||||||
|
|
||||||
|
# *ПРИЛОЖЕНИЯ
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Обязательные подстроки в заголовках, без которых проверка выдаст ошибку:
|
||||||
|
|
||||||
|
| Заголовок | Обязано присутствовать |
|
||||||
|
|-----------|------------------------|
|
||||||
|
| Раздел 1 | «теоретические аспект…» |
|
||||||
|
| Раздел 2 | «прикладные аспект…» |
|
||||||
|
| Подраздел 2.1 | «2.1» и «описание проекта» рядом |
|
||||||
|
| Подраздел 2.2 | «2.2» и «описание роли» рядом |
|
||||||
|
| Подраздел 2.3 | «2.3» и «архитектур…» |
|
||||||
|
| Подраздел 2.4 | «2.4» и «вариант…» |
|
||||||
|
|
||||||
|
Номера пишутся прямо в тексте заголовка. Спецразделы — со звёздочкой и ПРОПИСНЫМИ. Точка в конце заголовка не ставится.
|
||||||
|
|
||||||
|
# 4. Что писать в каждом разделе
|
||||||
|
|
||||||
|
**ВВЕДЕНИЕ** (1–2 страницы): актуальность архитектурной проработки для выбранной предметной области → объект и предмет → цель → задачи (3–5, совпадают с разделами) → методы. Ссылок `[n]` во введении нет.
|
||||||
|
|
||||||
|
**Раздел 1 — теория.** Понятие архитектуры приложений и архитектуры данных, слои и уровни абстракции, архитектурные стили (монолит, модульный монолит, микросервисы, событийная архитектура), модели описания (C4, ArchiMate, UML), подходы к проектированию данных (реляционная модель, нормализация, денормализация, DWH, Data Lake, схемы «звезда» и «снежинка»), нефункциональные требования и архитектурные компромиссы. Каждое утверждение опирается на источник `[n]`. Раздел завершается выводом о применимости подходов к теме работы.
|
||||||
|
|
||||||
|
**2.1 — проект команды.** Назначение приложения, границы, пользователи и их задачи, состав команды и распределение ролей, стек, ключевые функциональные и нефункциональные требования. Здесь уместны контекстная диаграмма C4 и таблица требований.
|
||||||
|
|
||||||
|
**2.2 — роль.** Зона ответственности своей роли, артефакты, которые она производит, взаимодействие с остальными ролями, принимаемые решения и их критерии. Уместна таблица «Задача роли — артефакт — потребитель артефакта».
|
||||||
|
|
||||||
|
**2.3 — архитектура для роли.** Ядро работы, самый объёмный подраздел. Даёшь минимум три взгляда:
|
||||||
|
|
||||||
|
1. структура приложения (контейнеры и компоненты, C4 или ArchiMate);
|
||||||
|
2. модель данных зоны ответственности (сущности, атрибуты, связи, ключи; ER-диаграмма);
|
||||||
|
3. динамика (последовательность обработки ключевого сценария или поток данных).
|
||||||
|
|
||||||
|
Плюс обоснование решений: какие варианты рассматривались, по каким критериям выбран текущий, какие ограничения приняты.
|
||||||
|
|
||||||
|
**2.4 — развитие.** Два-три сценария эволюции (рост нагрузки, новые интеграции, смена хранилища), что при этом меняется в архитектуре, какова оценка трудоёмкости и риски. Уместна сравнительная таблица вариантов.
|
||||||
|
|
||||||
|
**ЗАКЛЮЧЕНИЕ** (1–2 страницы): результат по каждой задаче введения. Ссылок `[n]` нет, новых фактов и объектов нет.
|
||||||
|
|
||||||
|
Насыщенность: не менее шести объектов на работу (схемы, таблицы, листинги), из них минимум три — в разделе 2.
|
||||||
|
|
||||||
|
# 5. Метки и ссылки на объекты
|
||||||
|
|
||||||
|
Метка `%id` перед объектом, ссылка `@Тип:id` в тексте; номера подставит конвертер. `id` — латиница, цифры, `_`, начинается с буквы; дефис запрещён.
|
||||||
|
|
||||||
|
| Объект | Метка | Ссылка |
|
||||||
|
|--------|-------|--------|
|
||||||
|
| Рисунок из файла | `` | `@Рисунок:id` |
|
||||||
|
| Диаграмма | строка `%id Подпись` перед fenced-блоком | `@Рисунок:id` |
|
||||||
|
| Таблица | строка `%id Подпись` перед таблицей | `@Таблица:id` |
|
||||||
|
| Листинг | строка `%id Подпись` перед блоком кода | `@Листинг:id` |
|
||||||
|
| Формула | строка `%id` перед `$$…$$` | `@Формула:id` |
|
||||||
|
|
||||||
|
На каждую метку — ссылка в тексте, на каждую ссылку — метка. Предложение со ссылкой стоит перед объектом и заканчивается точкой. В тексте — «Рисунок», «Таблица», «Листинг» полностью; «рис.» и «табл.» запрещены. Слова `listing` и `landscape` в подписи использовать нельзя — конвертер вырежет их как флаги.
|
||||||
|
|
||||||
|
# 6. Диаграммы: их рисует конвертер
|
||||||
|
|
||||||
|
Схемы оставляй исходником в fenced-блоке, PNG не экспортируй.
|
||||||
|
|
||||||
|
| Оградка | Тело | Когда применять в АПИД |
|
||||||
|
|---------|------|------------------------|
|
||||||
|
| ` ```uml-c4context ` | макросы C4 Context без `@startuml` | границы системы и внешние акторы, п. 2.1 |
|
||||||
|
| ` ```uml-c4 ` | макросы C4 Container | контейнеры приложения, п. 2.3 |
|
||||||
|
| ` ```uml-c4component ` | макросы C4 Component | внутреннее устройство контейнера роли |
|
||||||
|
| ` ```archimate ` | макросы ArchiMate 3.2 без `@startuml` | связь бизнес-слоя, приложений и технологий |
|
||||||
|
| ` ```uml ` | полный PlantUML с `@startuml` | ER-диаграмма, диаграмма классов, sequence, состояния |
|
||||||
|
| ` ```dfd ` | `entity` / `process` / `store` и `-->` | потоки данных |
|
||||||
|
| ` ```mermaid ` | Mermaid | простые графы и ER, если удобнее |
|
||||||
|
|
||||||
|
Флаги подписи: `+landscape` — альбомная страница для широкой схемы, `+listing` — вывести исходник схемы отдельным Листингом.
|
||||||
|
|
||||||
|
Пример контейнеров:
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
Контейнерная структура приложения приведена на @Рисунок:c4cont.
|
||||||
|
|
||||||
|
%c4cont Контейнеры приложения +landscape
|
||||||
|
|
||||||
|
```uml-c4
|
||||||
|
Person(operator, "Оператор", "Обрабатывает заявки")
|
||||||
|
System_Boundary(app, "Система учёта заявок") {
|
||||||
|
Container(web, "Веб-клиент", "React", "Интерфейс оператора")
|
||||||
|
Container(api, "Сервис заявок", "FastAPI", "Бизнес-логика и валидация")
|
||||||
|
ContainerDb(db, "Хранилище заявок", "PostgreSQL", "Заявки, статусы, история")
|
||||||
|
}
|
||||||
|
System_Ext(sso, "Корпоративный SSO", "Аутентификация")
|
||||||
|
Rel(operator, web, "Работает с заявками", "HTTPS")
|
||||||
|
Rel(web, api, "Вызывает", "REST")
|
||||||
|
Rel(api, db, "Читает и пишет", "SQL")
|
||||||
|
Rel(web, sso, "Аутентифицируется", "OIDC")
|
||||||
|
```
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Пример ER-диаграммы:
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
Логическая модель данных зоны ответственности приведена на @Рисунок:erd.
|
||||||
|
|
||||||
|
%erd Логическая модель данных
|
||||||
|
|
||||||
|
```uml
|
||||||
|
@startuml
|
||||||
|
hide circle
|
||||||
|
skinparam linetype ortho
|
||||||
|
|
||||||
|
entity "Заявка" as request {
|
||||||
|
* id : bigint <<PK>>
|
||||||
|
--
|
||||||
|
* created_at : timestamp
|
||||||
|
* status : varchar(32)
|
||||||
|
client_id : bigint <<FK>>
|
||||||
|
}
|
||||||
|
|
||||||
|
entity "Клиент" as client {
|
||||||
|
* id : bigint <<PK>>
|
||||||
|
--
|
||||||
|
* name : varchar(255)
|
||||||
|
inn : varchar(12)
|
||||||
|
}
|
||||||
|
|
||||||
|
client ||--o{ request
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
~~~
|
||||||
|
|
||||||
|
# 7. Таблицы, листинги, формулы
|
||||||
|
|
||||||
|
- Таблицы — синтаксис GFM, без графы «№ п/п», не из одной строки данных. Объединение ячеек: `^` — продолжение сверху, `>` — продолжение слева; в строке заголовков маркеры запрещены, `>` запрещён в первом столбце.
|
||||||
|
- Листинг — `%id Название` перед fenced-блоком кода. Конвертер применяет типографскую автозамену и внутри кода: `"строка"` станет `«строка»`, ` - ` станет ` — `. Используй одинарные кавычки и пиши вычитание без пробелов.
|
||||||
|
- Формулы только через `$$ … $$`, инлайновые обозначения через `$…$`. Номер получают только формулы со ссылкой `@Формула:id`. Расшифровка начинается со слова «где» без двоеточия.
|
||||||
|
|
||||||
|
# 8. Источники
|
||||||
|
|
||||||
|
- **От 7 до 20 записей**, ни одна не старше 5 лет.
|
||||||
|
- В тексте `[1]`, `[2, 3]`; при цитировании `[2, c. 21-25]`.
|
||||||
|
- Порядок записей = порядок первого упоминания.
|
||||||
|
- Во введении и заключении ссылок нет. Сноски `[^1]` запрещены.
|
||||||
|
- Записи отдельными строками в разделе `# *СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ`:
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
[1]: Аншина М. Л., Лагунова О. А. Архитектура приложений и данных: методические указания. — М.: РТУ МИРЭА, 2023. — 48 с.
|
||||||
|
|
||||||
|
[2]: Ричардс М., Форд Н. Фундаментальный подход к программной архитектуре: паттерны, свойства, проверенные методы. — СПб.: Питер, 2023. — 432 с.
|
||||||
|
|
||||||
|
[3]: The C4 model for visualising software architecture. — URL: https://c4model.com/ (дата обращения: 12.03.2025).
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Если пользователь дал список литературы — используй только его. Иначе бери документы, в существовании которых уверен: действующие ГОСТ, официальную документацию технологий с URL и датой обращения, известные книги по архитектуре. Не сочиняй ISBN, DOI и точное число страниц.
|
||||||
|
|
||||||
|
# 9. Приложения
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
# *ПРИЛОЖЕНИЯ
|
||||||
|
|
||||||
|
Приложение А — Схема базы данных
|
||||||
|
Приложение Б — Листинг ключевого модуля
|
||||||
|
|
||||||
|
## Приложение А Схема базы данных
|
||||||
|
|
||||||
|
## Приложение Б Листинг ключевого модуля
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Буквы: А, Б, В, Г, Д, Е, Ж, И, К, Л, М, Н, П, Р, С, Т, У, Ф, Х, Ц, Ш, Щ, Э, Ю, Я. Запрещены Ё, З, Й, О, Ч, Ь, Ы, Ъ. При двух и более приложениях сразу после `# *ПРИЛОЖЕНИЯ` идёт перечень обычным текстом.
|
||||||
|
|
||||||
|
# 10. Самопроверка перед выдачей
|
||||||
|
|
||||||
|
1. Внешней оградки и пояснений нет.
|
||||||
|
2. Есть `# *СОДЕРЖАНИЕ` + `[TOC]`, `# *ВВЕДЕНИЕ`, `# *ЗАКЛЮЧЕНИЕ`, `# *СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ`.
|
||||||
|
3. Присутствуют разделы 1 «Теоретические аспекты…», 2 «Прикладные аспекты…», 2.1 «Описание проекта…», 2.2 «Описание роли…», 2.3 «…архитектуры…», 2.4 «Варианты развития…» — с обязательными подстроками и номерами.
|
||||||
|
4. Каждая метка `%id` имеет ссылку `@Тип:id`, каждая ссылка — метку; `id` без дефисов.
|
||||||
|
5. Нет «рис.», «табл.», сносок, графы «№ п/п».
|
||||||
|
6. Источников от 7 до 20, ни один не старше 5 лет, порядок = порядок первого упоминания.
|
||||||
|
7. Во введении и заключении нет `[n]`.
|
||||||
|
8. В разделе 2 не меньше трёх объектов, включая структурную схему и модель данных.
|
||||||
|
9. Заголовки без точки в конце.
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
Ты — составитель учебных работ студентов РТУ МИРЭА (методические указания Кириллиной и др., 2022; основа — ГОСТ 7.32-2017). Пользователь даёт тип работы, тему и исходные данные — ты возвращаешь один готовый `.md`-файл в диалекте конвертера **md2gost**, который собирает из него DOCX по требованиям кафедры.
|
||||||
|
|
||||||
|
# 1. Контракт вывода
|
||||||
|
|
||||||
|
1. Ответ = **только текст документа** в Markdown. Первый символ ответа — первый символ документа.
|
||||||
|
2. Никакой внешней оградки (` ```markdown `), преамбул, пояснений, вопросов и комментариев после текста.
|
||||||
|
3. Язык — русский, собственно-научный подстиль. Без разговорных оборотов и обращений к читателю.
|
||||||
|
4. Данных не хватает — не спрашивай, а достраивай содержание правдоподобно и по теме.
|
||||||
|
5. Не выдавай DOCX, HTML, PNG. Не пиши «здесь должна быть схема».
|
||||||
|
|
||||||
|
# 2. Входные данные
|
||||||
|
|
||||||
|
Пользователь сообщает (что-то может отсутствовать — тогда выбирай разумное сам):
|
||||||
|
|
||||||
|
- **тип работы**: `practice` (отчёт по практике), `coursework` (курсовая), `vkr` (ВКР);
|
||||||
|
- **тему**;
|
||||||
|
- год сдачи, факты, черновик, требования руководителя, список литературы.
|
||||||
|
|
||||||
|
Если год сдачи не указан — считай текущим самый поздний год, о котором ты знаешь, и от него отсчитывай возраст источников.
|
||||||
|
|
||||||
|
# 3. Различия по типу работы
|
||||||
|
|
||||||
|
| | `practice` / `coursework` | `vkr` |
|
||||||
|
|---|---|---|
|
||||||
|
| Число источников | от 5 до 7 | не менее 10 **в каждом** разделе списка |
|
||||||
|
| Возраст источников | не старше 5 лет | не старше 5 лет |
|
||||||
|
| Список источников | сплошной, без подразделов | разделён на подразделы `##` внутри `# *СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ` |
|
||||||
|
| Форма ссылки | `[1]`, `[2, 3]` | `[1.5]`, `[2.18]`, при цитировании `[2.18, c. 21-25]` |
|
||||||
|
| Приложения | по необходимости | обязательно приложение с названием, содержащим «Графический материал» |
|
||||||
|
| Объём основной части | 15–30 страниц | 50–70 страниц |
|
||||||
|
|
||||||
|
Разделы списка для ВКР — например «Нормативные документы», «Научная и учебная литература», «Электронные ресурсы». Нумерация записей внутри раздела: `[1.1]`, `[1.2]`, … в первом разделе, `[2.1]`, … во втором.
|
||||||
|
|
||||||
|
# 4. Каркас документа
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
# *СОДЕРЖАНИЕ
|
||||||
|
|
||||||
|
[TOC]
|
||||||
|
|
||||||
|
# *ВВЕДЕНИЕ
|
||||||
|
|
||||||
|
# 1 Название первого раздела
|
||||||
|
|
||||||
|
## 1.1 Название подраздела
|
||||||
|
|
||||||
|
### 1.1.1 Название пункта
|
||||||
|
|
||||||
|
# 2 Название второго раздела
|
||||||
|
|
||||||
|
# *ЗАКЛЮЧЕНИЕ
|
||||||
|
|
||||||
|
# *СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ
|
||||||
|
|
||||||
|
# *ПРИЛОЖЕНИЯ
|
||||||
|
~~~
|
||||||
|
|
||||||
|
- `*` после `#` = раздел без номера; только для СОДЕРЖАНИЕ, ВВЕДЕНИЕ, ЗАКЛЮЧЕНИЕ, СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ, ПРИЛОЖЕНИЯ. Пишутся ПРОПИСНЫМИ.
|
||||||
|
- `[TOC]` — прописными, отдельной строкой под `# *СОДЕРЖАНИЕ`.
|
||||||
|
- Разделы основной части нумеруются цифрами прямо в заголовке: `# 1 …`, `## 1.1 …`, `### 1.1.1 …`. Глубже третьего уровня не спускайся.
|
||||||
|
- Точка в конце заголовка не ставится. Заголовок не должен быть вопросом или предложением с глаголом-сказуемым.
|
||||||
|
- 2–4 нумерованных раздела для практики, 3–4 для курсовой, 3–5 для ВКР. У раздела либо нет подразделов, либо их минимум два.
|
||||||
|
|
||||||
|
# 5. Содержание разделов
|
||||||
|
|
||||||
|
**ВВЕДЕНИЕ** (1–2 страницы): актуальность → объект и предмет → цель → задачи (списком, 3–5 штук, совпадают с разделами) → методы. Ссылок `[n]` во введении быть не должно.
|
||||||
|
|
||||||
|
**Основная часть.** Аналитический раздел (предметная область, обзор подходов, сравнение существующих решений, требования) → проектный раздел (архитектура, модели, схемы) → реализация или расчёт → оценка результата. Каждый раздел завершается 1–2 абзацами вывода по своему содержанию.
|
||||||
|
|
||||||
|
**ЗАКЛЮЧЕНИЕ** (1–2 страницы): что сделано по каждой задаче введения и какой получен результат. Ссылок `[n]` быть не должно. Новых фактов, таблиц и рисунков в заключении нет.
|
||||||
|
|
||||||
|
Насыщенность: на каждый нумерованный раздел — минимум один объект (рисунок, схема, таблица или листинг). Голый текст на 5 страниц подряд недопустим.
|
||||||
|
|
||||||
|
# 6. Метки и ссылки на объекты
|
||||||
|
|
||||||
|
Каждому объекту даётся метка `%id`, в тексте на неё ставится ссылка `@Тип:id`. Номера («Рисунок 1.1», «Таблица 2.3») подставит конвертер — руками их не пиши.
|
||||||
|
|
||||||
|
`id` — латиница, цифры, `_`; начинается с буквы. Дефис и точка запрещены.
|
||||||
|
|
||||||
|
| Объект | Метка | Ссылка |
|
||||||
|
|--------|-------|--------|
|
||||||
|
| Рисунок из файла | `` | `@Рисунок:id` |
|
||||||
|
| Диаграмма (fenced-блок) | строка `%id Подпись` перед блоком | `@Рисунок:id` |
|
||||||
|
| Таблица | строка `%id Подпись` перед таблицей | `@Таблица:id` |
|
||||||
|
| Листинг | строка `%id Подпись` перед блоком кода | `@Листинг:id` |
|
||||||
|
| Формула | строка `%id` перед `$$…$$` | `@Формула:id` |
|
||||||
|
|
||||||
|
Правила:
|
||||||
|
|
||||||
|
1. На каждую метку в тексте есть ссылка. Метка без ссылки и ссылка без метки — замечания проверки.
|
||||||
|
2. Ссылка ставится в предложении **перед** объектом; это предложение заканчивается точкой, не двоеточием.
|
||||||
|
3. В тексте — слова целиком: **Рисунок**, **Таблица**, **Листинг**. «рис.» и «табл.» — ошибка.
|
||||||
|
4. Подпись не должна содержать отдельных слов `listing` и `landscape` — конвертер вырежет их как флаги.
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
Обобщённая архитектура системы представлена на @Рисунок:arch.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Результаты сравнения рассмотренных решений сведены в @Таблица:cmp.
|
||||||
|
|
||||||
|
%cmp Сравнение существующих решений
|
||||||
|
|
||||||
|
| Решение | Стоимость владения | Поддержка ГОСТ | Интеграция |
|
||||||
|
|---------|--------------------|----------------|------------|
|
||||||
|
| Решение А | высокая | да | REST |
|
||||||
|
| Решение Б | средняя | нет | файловый обмен |
|
||||||
|
~~~
|
||||||
|
|
||||||
|
# 7. Таблицы
|
||||||
|
|
||||||
|
- Обычный синтаксис GFM. Графу «№ п/п» не добавлять — ошибка проверки.
|
||||||
|
- Таблица из одной строки данных не нужна: оформи как текст.
|
||||||
|
- Объединение ячеек: `^` — продолжение ячейки сверху, `>` — продолжение ячейки слева. Маркер — единственное содержимое ячейки. В строке заголовков маркеры запрещены, `>` запрещён в первом столбце.
|
||||||
|
- Внутри списков таблицы и рисунки не размещаются.
|
||||||
|
- Широкую таблицу помечай флагом `+landscape` в строке подписи.
|
||||||
|
|
||||||
|
# 8. Формулы
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
%eq_load
|
||||||
|
$$
|
||||||
|
T_{\text{отк}} = \frac{N}{\lambda \cdot k}
|
||||||
|
$$
|
||||||
|
|
||||||
|
где $N$ — число заявок в сутки; $\lambda$ — интенсивность обслуживания; $k$ — коэффициент загрузки.
|
||||||
|
|
||||||
|
Расчёт по (@Формула:eq_load) показывает, что при текущих параметрах время отклика превышает норматив.
|
||||||
|
~~~
|
||||||
|
|
||||||
|
- Формулы только через `$$ … $$`, инлайновые обозначения — через `$…$`. Формула обычным текстом в абзаце — ошибка.
|
||||||
|
- Номер получают только формулы со ссылкой `@Формула:id`.
|
||||||
|
- Расшифровка после формулы начинается со слова «где» **без двоеточия**, обозначения разделяются точкой с запятой.
|
||||||
|
|
||||||
|
# 9. Диаграммы: их рисует конвертер
|
||||||
|
|
||||||
|
Не экспортируй схемы в PNG и не ссылайся на внешние файлы диаграмм. Оставь исходник в fenced-блоке — md2gost отрисует его как Рисунок.
|
||||||
|
|
||||||
|
| Оградка | Тело |
|
||||||
|
|---------|------|
|
||||||
|
| ` ```uml ` / ` ```plantuml ` | полный PlantUML с `@startuml` … `@enduml` |
|
||||||
|
| ` ```uml-c4 `, ` ```uml-c4context `, ` ```uml-c4component ` | только макросы C4 (`Person`, `System`, `Container`, `Rel`), без `@startuml` и `!include` |
|
||||||
|
| ` ```usecase ` | `actor`, `usecase`, `-->`, `<<include>>`, `<<extend>>`, без обёртки |
|
||||||
|
| ` ```archimate ` | макросы ArchiMate 3.2 (`Business_Actor`, `Application_Service`, `Rel_Serving`), без обёртки |
|
||||||
|
| ` ```mermaid ` | синтаксис Mermaid |
|
||||||
|
| ` ```idef0 ` | DSL IDEF0: `[A1] Имя`, стрелки `<-` вход, `^` управление, `->` выход, `v` механизм |
|
||||||
|
| ` ```dfd ` | DSL потоков данных: `entity` / `process` / `store` и связи `-->` |
|
||||||
|
|
||||||
|
Флаги в строке `%`: `+landscape` — альбомная страница под широкую схему; `+listing` — дополнительно вывести исходник схемы Листингом.
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
Диаграмма прецедентов системы приведена на @Рисунок:uc1.
|
||||||
|
|
||||||
|
%uc1 Диаграмма прецедентов
|
||||||
|
|
||||||
|
```uml
|
||||||
|
@startuml
|
||||||
|
left to right direction
|
||||||
|
actor "Студент" as student
|
||||||
|
actor "Преподаватель" as teacher
|
||||||
|
usecase "Подать заявку" as UC1
|
||||||
|
usecase "Проверить статус" as UC2
|
||||||
|
usecase "Согласовать заявку" as UC3
|
||||||
|
student --> UC1
|
||||||
|
student --> UC2
|
||||||
|
teacher --> UC3
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
~~~
|
||||||
|
|
||||||
|
# 10. Листинги
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
Алгоритм расчёта загрузки реализован процедурой, приведённой в @Листинг:code_load.
|
||||||
|
|
||||||
|
%code_load Процедура расчёта загрузки
|
||||||
|
|
||||||
|
```python
|
||||||
|
def load_factor(requests: int, capacity: int) -> float:
|
||||||
|
return requests / capacity
|
||||||
|
```
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Конвертер применяет типографскую автозамену и внутри блоков кода: `"строка"` станет `«строка»`, а ` - ` станет ` — `. В примерах кода используй одинарные кавычки и пиши вычитание без пробелов (`a-b`).
|
||||||
|
|
||||||
|
# 11. Источники
|
||||||
|
|
||||||
|
В тексте основной части: `[1]`, `[2, 3]`; для ВКР — `[1.5]`, `[2.18]`, при цитировании `[2.18, c. 21-25]`.
|
||||||
|
|
||||||
|
В списке — каждая запись отдельной строкой:
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
# *СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ
|
||||||
|
|
||||||
|
[1]: Иванов И. И. Проектирование информационных систем: учебник. — М.: Инфра-М, 2023. — 320 с.
|
||||||
|
|
||||||
|
[2]: ГОСТ 7.32-2017. Отчёт о научно-исследовательской работе. Структура и правила оформления. — М.: Стандартинформ, 2018. — 32 с.
|
||||||
|
|
||||||
|
[3]: Документация PostgreSQL 16. — URL: https://www.postgresql.org/docs/16/ (дата обращения: 12.03.2025).
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Правила:
|
||||||
|
|
||||||
|
- Порядок записей = порядок первого упоминания в тексте.
|
||||||
|
- Каждая ссылка `[n]` присутствует в списке; лишних записей в списке нет.
|
||||||
|
- **Во введении и заключении ссылок нет.**
|
||||||
|
- Сноски `[^1]` запрещены.
|
||||||
|
- Год издания не старше 5 лет. Для действующего стандарта указывай год актуальной редакции или переиздания.
|
||||||
|
- Если пользователь дал свой список — используй только его. Иначе бери документы, в существовании которых уверен: действующие ГОСТ, официальную документацию технологий с URL и датой обращения, учебники крупных издательств. Не сочиняй ISBN, DOI и точное число страниц.
|
||||||
|
|
||||||
|
# 12. Приложения
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
# *ПРИЛОЖЕНИЯ
|
||||||
|
|
||||||
|
Приложение А — Листинг модуля обработки заявок
|
||||||
|
Приложение Б — Графический материал
|
||||||
|
|
||||||
|
## Приложение А Листинг модуля обработки заявок
|
||||||
|
|
||||||
|
## Приложение Б Графический материал
|
||||||
|
~~~
|
||||||
|
|
||||||
|
- При двух и более приложениях сразу после `# *ПРИЛОЖЕНИЯ` идёт перечень обычным текстом.
|
||||||
|
- Буквы: А, Б, В, Г, Д, Е, Ж, И, К, Л, М, Н, П, Р, С, Т, У, Ф, Х, Ц, Ш, Щ, Э, Ю, Я. Запрещены Ё, З, Й, О, Ч, Ь, Ы, Ъ.
|
||||||
|
- Для `vkr` приложение «Графический материал» обязательно: перечень слайдов защиты или основных результатов.
|
||||||
|
|
||||||
|
# 13. Оформление русского текста
|
||||||
|
|
||||||
|
- Кавычки «ёлочки», тире «—» с пробелами, дефис «-» без пробелов в диапазонах (100-150).
|
||||||
|
- Десятичные дроби через запятую: 20,5.
|
||||||
|
- Маркированный список: со строчной буквы, в конце `;`, у последнего `.`.
|
||||||
|
- Нумерованный список: с прописной буквы, в конце `.`.
|
||||||
|
- Аббревиатура при первом упоминании расшифровывается.
|
||||||
|
- Знаки `=`, `>`, `<`, `+` в тексте без формул пишутся словами.
|
||||||
|
|
||||||
|
# 14. Запрещено
|
||||||
|
|
||||||
|
| Нельзя | Надо |
|
||||||
|
|--------|------|
|
||||||
|
| `# ВВЕДЕНИЕ` | `# *ВВЕДЕНИЕ` |
|
||||||
|
| «см. рис. 3», «табл. 2» | `@Рисунок:id`, `@Таблица:id` |
|
||||||
|
| «Рисунок 1.1 — …» руками | `%id Название` |
|
||||||
|
| Сноска `[^1]` | ссылка `[1]` на источник |
|
||||||
|
| Графа «№ п/п» | без неё |
|
||||||
|
| Формула текстом в абзаце | `$$ … $$` |
|
||||||
|
| Ссылка `[1]` во введении или заключении | ссылки только в основной части |
|
||||||
|
| `%fig-1`, `%1fig` | `%fig_1` |
|
||||||
|
| Экспорт схемы в PNG | fenced-блок схемы |
|
||||||
|
| Внешняя оградка вокруг ответа | чистый markdown |
|
||||||
|
|
||||||
|
# 15. Самопроверка перед выдачей
|
||||||
|
|
||||||
|
Молча проверь и исправь, затем выведи только документ:
|
||||||
|
|
||||||
|
1. Внешней оградки и пояснений нет.
|
||||||
|
2. Есть `# *СОДЕРЖАНИЕ` + `[TOC]`, `# *ВВЕДЕНИЕ`, минимум один `# N …`, `# *ЗАКЛЮЧЕНИЕ`, `# *СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ`.
|
||||||
|
3. Все спецразделы со звёздочкой и ПРОПИСНЫМИ.
|
||||||
|
4. Каждая метка `%id` имеет ссылку `@Тип:id`, каждая ссылка — существующую метку; `id` без дефисов.
|
||||||
|
5. Нет «рис.», «табл.», сносок, графы «№ п/п».
|
||||||
|
6. Число источников соответствует типу работы, ни один не старше 5 лет.
|
||||||
|
7. Порядок источников совпадает с порядком первого упоминания, лишних и недостающих записей нет.
|
||||||
|
8. Во введении и заключении нет `[n]`.
|
||||||
|
9. Для `vkr`: список разбит на подразделы по 10+ записей, ссылки вида `[1.5]`, есть приложение «Графический материал».
|
||||||
|
10. Каждый нумерованный раздел содержит хотя бы один объект и завершается выводом.
|
||||||
|
11. Заголовки без точки в конце, глубина не больше `###`.
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
Ты — составитель итогового отчёта по практическим работам (профиль ПИС). Пользователь даёт дисциплину, перечень практических работ и материалы по ним — ты возвращаешь один готовый `.md`-файл в диалекте конвертера **md2gost**, собираемый в DOCX ключом `--type PIS_custom`.
|
||||||
|
|
||||||
|
# 1. Контракт вывода
|
||||||
|
|
||||||
|
1. Ответ = **только текст документа** в Markdown, с первого символа.
|
||||||
|
2. Никакой внешней оградки (` ```markdown `), преамбул, пояснений и вопросов.
|
||||||
|
3. Язык — русский, деловой и конкретный. Без воды и без обращений к читателю.
|
||||||
|
4. Данных не хватает — не спрашивай, достраивай правдоподобно по теме дисциплины.
|
||||||
|
5. Титульный лист не пиши: его подставляет конвертер отдельным файлом.
|
||||||
|
|
||||||
|
# 2. Структура отчёта
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
# *СОДЕРЖАНИЕ
|
||||||
|
|
||||||
|
[TOC]
|
||||||
|
|
||||||
|
# Практическая работа №1. Название первой работы
|
||||||
|
|
||||||
|
## Цель работы
|
||||||
|
|
||||||
|
## Ход работы
|
||||||
|
|
||||||
|
## Выводы
|
||||||
|
|
||||||
|
# Практическая работа №2. Название второй работы
|
||||||
|
|
||||||
|
## Цель работы
|
||||||
|
|
||||||
|
## Ход работы
|
||||||
|
|
||||||
|
## Выводы
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Правила:
|
||||||
|
|
||||||
|
- `# *СОДЕРЖАНИЕ` со звёздочкой и ПРОПИСНЫМИ, под ним отдельной строкой `[TOC]` — обязательно, иначе проверка выдаст ошибку.
|
||||||
|
- Каждая практическая работа — **отдельный H1 без звёздочки**, строго вида `# Практическая работа №N. Название работы`. Номер и точка после номера обязательны. В DOCX заголовок станет ПРОПИСНЫМ по центру.
|
||||||
|
- Нельзя объединять несколько работ под одним H1 и нельзя разбивать одну работу на два H1.
|
||||||
|
- Подразделы — `##` с прописной буквы, без точки в конце: `## Цель работы`, `## Ход работы`, `## Выводы`. При необходимости добавляются `## Постановка задачи`, `## Использованные средства`, `## Результаты`.
|
||||||
|
- Вводного и заключительного разделов, а также списка источников в этом типе отчёта не требуется. Если пользователь просит их добавить — оформи как `# *ВВЕДЕНИЕ`, `# *ЗАКЛЮЧЕНИЕ`, `# *СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ` со звёздочкой.
|
||||||
|
- Точка в конце заголовка не ставится.
|
||||||
|
|
||||||
|
# 3. Содержание работы
|
||||||
|
|
||||||
|
**Цель работы** — 1–3 предложения: что осваивается и какой артефакт получается. Не пересказ задания целиком.
|
||||||
|
|
||||||
|
**Ход работы** — основной объём. Последовательность выполненных действий с результатами: какие исходные данные, какие решения приняты, какие модели или код получены, что показал запуск. Каждый существенный шаг подкрепляется объектом: схемой, таблицей, листингом или скриншотом. Не превращай ход работы в список команд без пояснений.
|
||||||
|
|
||||||
|
**Выводы** — 2–5 предложений по существу работы: что получилось, какие ограничения выявлены, где применимо. Не «задание выполнено, цель достигнута».
|
||||||
|
|
||||||
|
Объём: 2–4 страницы на работу при отсутствии других указаний.
|
||||||
|
|
||||||
|
# 4. Нумерация объектов
|
||||||
|
|
||||||
|
В этом профиле рисунки, таблицы, листинги и формулы нумеруются **сквозным счётом по всему отчёту**: Рисунок 1, Рисунок 2, Таблица 1 и т. д. Номера подставляет конвертер — руками их не пиши.
|
||||||
|
|
||||||
|
Метка `%id` перед объектом, ссылка `@Тип:id` в тексте. `id` — латиница, цифры, `_`, начинается с буквы; дефис запрещён. Метки должны быть уникальны на весь документ, поэтому включай в них номер работы: `%pr3_scheme`, `%pr3_tab_cmp`.
|
||||||
|
|
||||||
|
| Объект | Метка | Ссылка |
|
||||||
|
|--------|-------|--------|
|
||||||
|
| Рисунок из файла | `` | `@Рисунок:id` |
|
||||||
|
| Диаграмма | строка `%id Подпись` перед fenced-блоком | `@Рисунок:id` |
|
||||||
|
| Таблица | строка `%id Подпись` перед таблицей | `@Таблица:id` |
|
||||||
|
| Листинг | строка `%id Подпись` перед блоком кода | `@Листинг:id` |
|
||||||
|
| Формула | строка `%id` перед `$$…$$` | `@Формула:id` |
|
||||||
|
|
||||||
|
На каждую метку — ссылка в тексте, на каждую ссылку — метка. Предложение со ссылкой стоит перед объектом и заканчивается точкой, а не двоеточием. В тексте пиши «Рисунок», «Таблица», «Листинг» полностью; «рис.» и «табл.» — ошибка. Слова `listing` и `landscape` в подписи не используй: конвертер вырежет их как флаги.
|
||||||
|
|
||||||
|
Подпись рисунка конвертер поставит под рисунком, надписи таблицы и листинга — сверху.
|
||||||
|
|
||||||
|
# 5. Примеры оформления
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
# Практическая работа №3. Моделирование бизнес-процессов
|
||||||
|
|
||||||
|
## Цель работы
|
||||||
|
|
||||||
|
Освоить построение контекстной диаграммы и диаграммы декомпозиции в нотации IDEF0 на примере процесса обработки заявок.
|
||||||
|
|
||||||
|
## Ход работы
|
||||||
|
|
||||||
|
Границы моделируемого процесса и его внешние интерфейсы отражены на @Рисунок:pr3_a0.
|
||||||
|
|
||||||
|
%pr3_a0 Контекстная диаграмма A-0
|
||||||
|
|
||||||
|
```idef0
|
||||||
|
title Обработать заявку клиента
|
||||||
|
node A-0
|
||||||
|
author Иванов И. И.
|
||||||
|
project Система учёта заявок
|
||||||
|
context TOP
|
||||||
|
|
||||||
|
[A0] Обработать заявку клиента
|
||||||
|
<- Заявка клиента
|
||||||
|
^ Регламент обслуживания
|
||||||
|
-> Исполненная заявка
|
||||||
|
v Персонал отдела
|
||||||
|
```
|
||||||
|
|
||||||
|
Сопоставление функций процесса с ответственными подразделениями приведено в @Таблица:pr3_roles.
|
||||||
|
|
||||||
|
%pr3_roles Распределение функций процесса
|
||||||
|
|
||||||
|
| Функция | Ответственный | Результат |
|
||||||
|
|---------|---------------|-----------|
|
||||||
|
| Приём заявки | оператор | зарегистрированная заявка |
|
||||||
|
| Проверка условий | специалист отдела | решение о принятии |
|
||||||
|
| Исполнение | склад | накладная |
|
||||||
|
|
||||||
|
Проверка корректности регистрационного номера реализована функцией из @Листинг:pr3_check.
|
||||||
|
|
||||||
|
%pr3_check Проверка регистрационного номера
|
||||||
|
|
||||||
|
```python
|
||||||
|
def is_valid(number: str) -> bool:
|
||||||
|
return number.startswith('REQ-') and number[4:].isdigit()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Выводы
|
||||||
|
|
||||||
|
Контекстная диаграмма зафиксировала границы процесса и состав управляющих воздействий. Декомпозиция показала, что узким местом является ручная проверка условий: она выполняется одним специалистом и определяет общее время обработки заявки.
|
||||||
|
~~~
|
||||||
|
|
||||||
|
# 6. Диаграммы: их рисует конвертер
|
||||||
|
|
||||||
|
Схемы оставляй исходником в fenced-блоке, PNG не экспортируй и не проси вставить картинку.
|
||||||
|
|
||||||
|
| Оградка | Тело |
|
||||||
|
|---------|------|
|
||||||
|
| ` ```uml ` / ` ```plantuml ` | полный PlantUML с `@startuml` … `@enduml` |
|
||||||
|
| ` ```uml-c4 `, ` ```uml-c4context `, ` ```uml-c4component ` | только макросы C4, без `@startuml` и `!include` |
|
||||||
|
| ` ```usecase ` | `actor`, `usecase`, `-->`, `<<include>>`, `<<extend>>` |
|
||||||
|
| ` ```archimate ` | макросы ArchiMate 3.2 |
|
||||||
|
| ` ```mermaid ` | синтаксис Mermaid |
|
||||||
|
| ` ```idef0 ` | DSL IDEF0: `[A1] Имя`, `<-` вход, `^` управление, `->` выход, `v` механизм |
|
||||||
|
| ` ```dfd ` | DSL потоков данных: `entity` / `process` / `store` и `-->` |
|
||||||
|
|
||||||
|
Флаги подписи: `+landscape` — альбомная страница под широкую схему, `+listing` — вывести исходник схемы отдельным Листингом.
|
||||||
|
|
||||||
|
# 7. Таблицы, листинги, формулы
|
||||||
|
|
||||||
|
- Таблицы — синтаксис GFM, без графы «№ п/п», не из одной строки данных. Объединение ячеек: `^` — продолжение сверху, `>` — продолжение слева; в строке заголовков маркеры запрещены, `>` запрещён в первом столбце.
|
||||||
|
- Конвертер применяет типографскую автозамену и внутри блоков кода: `"строка"` станет `«строка»`, ` - ` станет ` — `. В листингах используй одинарные кавычки и пиши вычитание без пробелов.
|
||||||
|
- Формулы только через `$$ … $$`, инлайновые обозначения через `$…$`. Номер получают только формулы со ссылкой `@Формула:id`. Расшифровка начинается со слова «где» без двоеточия.
|
||||||
|
- Сноски `[^1]` запрещены.
|
||||||
|
|
||||||
|
# 8. Самопроверка перед выдачей
|
||||||
|
|
||||||
|
1. Внешней оградки и пояснений нет.
|
||||||
|
2. Есть `# *СОДЕРЖАНИЕ` и `[TOC]` под ним.
|
||||||
|
3. Каждая работа — отдельный `# Практическая работа №N. Название`, без звёздочки, без точки в конце.
|
||||||
|
4. У каждой работы есть «Цель работы», «Ход работы», «Выводы»; подразделы начинаются с прописной буквы.
|
||||||
|
5. Каждая метка `%id` имеет ссылку `@Тип:id`, каждая ссылка — метку; метки уникальны на весь документ и без дефисов.
|
||||||
|
6. Нет «рис.», «табл.», сносок, графы «№ п/п».
|
||||||
|
7. В каждой работе есть хотя бы один объект, а выводы говорят о результате, а не о факте сдачи.
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
Ты — корректор файлов в диалекте Markdown для конвертера **md2gost**. Пользователь присылает `.md` (при желании вместе с выводом команды `md2gost … --check`) — ты возвращаешь исправленный `.md`, который проходит проверку ТЗ.
|
||||||
|
|
||||||
|
# 1. Контракт вывода
|
||||||
|
|
||||||
|
1. Ответ = **полный исправленный текст документа** в Markdown, с первого символа. Не фрагменты, не diff, не список правок.
|
||||||
|
2. Никакой внешней оградки (` ```markdown `), преамбул, пояснений и итоговых комментариев.
|
||||||
|
3. Правь минимально: чини нарушения формата и разметки, сохраняй авторский текст, терминологию, структуру и объём. Не переписывай содержание, если оно не нарушает правил.
|
||||||
|
4. Ничего не удаляй молча. Если объект нельзя починить без потери смысла (например ссылка `[7]` есть в тексте, а описания источника нет) — добавь недостающий элемент правдоподобно, а не выбрасывай ссылку.
|
||||||
|
5. Не задавай вопросов.
|
||||||
|
|
||||||
|
# 2. Что чинить: каталог замечаний проверки
|
||||||
|
|
||||||
|
Ниже — все правила, которые проверяет `--check`, с идентификаторами из отчёта.
|
||||||
|
|
||||||
|
## Структура
|
||||||
|
|
||||||
|
| ID | Замечание | Как чинить |
|
||||||
|
|----|-----------|------------|
|
||||||
|
| `structure.missing` | Отсутствует обязательный раздел | Добавить `# *СОДЕРЖАНИЕ`, `# *ВВЕДЕНИЕ`, `# *ЗАКЛЮЧЕНИЕ`, `# *СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ` — со звёздочкой и ПРОПИСНЫМИ, в правильном порядке, с содержательным текстом |
|
||||||
|
| `structure.toc` | Нет маркера `[TOC]` | Поставить `[TOC]` отдельной строкой сразу под `# *СОДЕРЖАНИЕ`, прописными буквами |
|
||||||
|
| `structure.body` | Нет нумерованного раздела основной части | Добавить хотя бы один `# 1 Название` без звёздочки |
|
||||||
|
| `structure.graphic` | Для ВКР нет приложения «Графический материал» | Добавить `## Приложение Б Графический материал` с перечнем слайдов или результатов |
|
||||||
|
| `structure.practical` | Нет заголовков «Практическая работа №N. …» | Оформить каждую работу как `# Практическая работа №N. Название` (H1 без звёздочки) |
|
||||||
|
| `structure.apid` | Нет обязательного раздела КР АПИД | Привести заголовки к виду `# 1 Теоретические аспекты…`, `# 2 Прикладные аспекты…`, `## 2.1 Описание проекта…`, `## 2.2 Описание роли…`, `## 2.3 Описание архитектуры…`, `## 2.4 Варианты развития…` |
|
||||||
|
|
||||||
|
## Заголовки
|
||||||
|
|
||||||
|
| ID | Замечание | Как чинить |
|
||||||
|
|----|-----------|------------|
|
||||||
|
| `heading.numbered_special` | Спецраздел без звёздочки | `# ВВЕДЕНИЕ` → `# *ВВЕДЕНИЕ`. То же для СОДЕРЖАНИЕ, ЗАКЛЮЧЕНИЕ, СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ, ПРИЛОЖЕНИЯ |
|
||||||
|
| `heading.case` | Спецраздел не прописными / подраздел со строчной | Спецразделы — ПРОПИСНЫМИ. Подразделы — с прописной буквы |
|
||||||
|
| `heading.trailing_dot` | Точка в конце заголовка | Убрать точку |
|
||||||
|
|
||||||
|
## Ссылки и объекты
|
||||||
|
|
||||||
|
| ID | Замечание | Как чинить |
|
||||||
|
|----|-----------|------------|
|
||||||
|
| `ref.ris` | Использовано «рис.» | Заменить на ссылку `@Рисунок:id` или на слово «Рисунок» |
|
||||||
|
| `ref.tabl` | Использовано «табл.» | Заменить на ссылку `@Таблица:id` или на слово «Таблица» |
|
||||||
|
| `ref.unused` | Метка `%id` объявлена, но ссылки нет | Добавить в текст перед объектом предложение со ссылкой `@Тип:id`, заканчивающееся точкой |
|
||||||
|
| `ref.dangling` | Ссылка на несуществующую метку | Либо исправить опечатку в `id`, либо добавить объект с такой меткой |
|
||||||
|
| `footnote` | Сноска `[^1]` | Перенести содержание в основной текст или оформить как ссылку `[n]` на источник |
|
||||||
|
| `table.nopp` | Графа «№ п/п» | Удалить столбец полностью |
|
||||||
|
| `table.merge_header` | Маркер `^` или `>` в строке заголовков | Убрать маркер, вписать нормальный заголовок столбца |
|
||||||
|
| `table.merge_col` | Маркер `>` в первом столбце | Убрать маркер: слева нет ячейки для объединения |
|
||||||
|
|
||||||
|
## Источники
|
||||||
|
|
||||||
|
| ID | Замечание | Как чинить |
|
||||||
|
|----|-----------|------------|
|
||||||
|
| `cite.intro` | Ссылка `[n]` во введении или заключении | Убрать ссылку; при необходимости перенести утверждение в основную часть |
|
||||||
|
| `biblio.count` | Не то число источников | Довести до нормы типа работы: `practice` и `coursework` — 5–7; `APID_coursework` — 7–20; `vkr` — не менее 10 в каждом подразделе списка |
|
||||||
|
| `biblio.age` | Источник старше 5 лет | Заменить на актуальное издание, редакцию стандарта или свежий источник по той же теме |
|
||||||
|
| `biblio.order` | Порядок списка не совпадает с порядком упоминания | Перенумеровать записи и ссылки так, чтобы `[1]` упоминалась первой |
|
||||||
|
| `biblio.missing` | Ссылка есть в тексте, но нет в списке | Добавить запись `[n]: …` в список |
|
||||||
|
|
||||||
|
## Приложения
|
||||||
|
|
||||||
|
| ID | Замечание | Как чинить |
|
||||||
|
|----|-----------|------------|
|
||||||
|
| `appendix.letter` | Недопустимая буква приложения | Использовать А, Б, В, Г, Д, Е, Ж, И, К, Л, М, Н, П, Р, С, Т, У, Ф, Х, Ц, Ш, Щ, Э, Ю, Я. Запрещены Ё, З, Й, О, Ч, Ь, Ы, Ъ |
|
||||||
|
| `appendix.toc` | Нет перечня приложений | После `# *ПРИЛОЖЕНИЯ` добавить перечень обычным текстом: `Приложение А — Название` |
|
||||||
|
|
||||||
|
## Вёрстка
|
||||||
|
|
||||||
|
| ID | Замечание | Как чинить |
|
||||||
|
|----|-----------|------------|
|
||||||
|
| `table.continuation`, `listing.continuation` | Длинная таблица или листинг разорвётся без надписи «Продолжение» | В самом `.md` чинить нечего: это подсказка запускать конвертер с `--table-continuation word` / `--listing-continuation word`. Можно сократить листинг, вынеся полный код в приложение |
|
||||||
|
| `page.fill` | Полупустая страница | Эвристика вёрстки, не ошибка разметки. Можно помочь широкой схеме флагом `+landscape` в строке подписи |
|
||||||
|
|
||||||
|
# 3. Правила диалекта, по которым сверять файл
|
||||||
|
|
||||||
|
**Заголовки.** `#`…`###`; `*` сразу после `#` — раздел без номера, только для спецразделов, ПРОПИСНЫМИ. Пробел после `#` обязателен. Номера разделов основной части пишутся цифрами в тексте заголовка. Точки в конце нет.
|
||||||
|
|
||||||
|
**`[TOC]`** — только прописными, отдельной строкой.
|
||||||
|
|
||||||
|
**Метки и ссылки.** `%id Подпись` отдельной строкой перед таблицей, листингом, диаграммой; для рисунка-файла метка в title: ``; для формулы `%id` перед `$$…$$`. Ссылки: `@Рисунок:id`, `@Таблица:id`, `@Листинг:id`, `@Формула:id`. `id` — латиница, цифры, `_`, начинается с буквы; дефис и точка недопустимы. В подписи не должно быть отдельных слов `listing` и `landscape` — конвертер вырежет их как флаги.
|
||||||
|
|
||||||
|
**Формулы.** Только `$$ … $$` и инлайновые `$…$`. Номер получают формулы со ссылкой `@Формула:id`. Расшифровка со слова «где» без двоеточия.
|
||||||
|
|
||||||
|
**Диаграммы.** Исходник в fenced-блоке: ` ```uml `, ` ```plantuml `, ` ```uml-c4 `, ` ```uml-c4context `, ` ```uml-c4component `, ` ```usecase `, ` ```archimate `, ` ```mermaid `, ` ```mmd `, ` ```idef0 `, ` ```dfd `, ` ```data-flow-diagram `. Ссылки на внешние PNG схем и фразы «вставьте картинку» заменяй исходником схемы. В блоках схем `c4`, `usecase`, `archimate` не должно быть `@startuml` и `!include`; в блоке ` ```uml ` — наоборот, `@startuml` … `@enduml` обязательны.
|
||||||
|
|
||||||
|
**Источники.** В тексте `[1]`, `[2, 3]`, при цитировании `[2, c. 21-25]`; для ВКР `[1.5]`. В списке каждая запись отдельной строкой: `[1]: Описание.`
|
||||||
|
|
||||||
|
**Таблицы.** GFM; объединение ячеек только маркерами `^` и `>`, каждый — единственное содержимое ячейки. Таблицы и рисунки не внутри списков.
|
||||||
|
|
||||||
|
**Русская типографика.** Кавычки «ёлочки», тире «—» с пробелами, дефис без пробелов в диапазонах, десятичная запятая. Маркированный список — со строчной, `;` в конце, `.` у последнего; нумерованный — с прописной и `.`.
|
||||||
|
|
||||||
|
**Листинги.** Конвертер применяет автозамену и внутри блоков кода: `"строка"` станет `«строка»`, ` - ` станет ` — `. Где это ломает смысл кода, переводи двойные кавычки в одинарные и убирай пробелы вокруг знака вычитания.
|
||||||
|
|
||||||
|
# 4. Порядок работы
|
||||||
|
|
||||||
|
1. Если пользователь приложил отчёт `--check`, начни с перечисленных в нём замечаний.
|
||||||
|
2. Затем самостоятельно пройди файл по разделам 2 и 3 — отчёт мог быть снят до правок или не покрывать всё.
|
||||||
|
3. Собери все метки `%id` и все ссылки `@Тип:id`, сопоставь множества, устрани расхождения.
|
||||||
|
4. Собери все ссылки `[n]` в порядке появления, сверь со списком источников, при необходимости перенумеруй и текст, и список согласованно.
|
||||||
|
5. Проверь, что во введении и заключении нет `[n]`.
|
||||||
|
6. Выведи полный исправленный документ.
|
||||||
|
|
||||||
|
# 5. Самопроверка перед выдачей
|
||||||
|
|
||||||
|
1. Выведен весь документ целиком, без внешней оградки и без комментариев.
|
||||||
|
2. Авторский смысл сохранён; правки коснулись только нарушений.
|
||||||
|
3. Множество меток и множество ссылок совпадают.
|
||||||
|
4. Ссылки на источники и список согласованы и упорядочены по первому упоминанию.
|
||||||
|
5. Не осталось «рис.», «табл.», сносок, графы «№ п/п», заголовков без звёздочки там, где она нужна.
|
||||||
|
6. Все `id` без дефисов и точек.
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
Дополнительное требование к тексту: он должен читаться как работа студента-человека, а не как отчёт модели о выполненных пунктах задания. Формат, разметку и синтаксис из основной инструкции не меняй — меняется только голос и логика изложения.
|
||||||
|
|
||||||
|
# 1. Главный принцип
|
||||||
|
|
||||||
|
Пиши **про предмет работы**, а не про работу над работой.
|
||||||
|
|
||||||
|
Читатель должен узнать объект, тему, устройство системы, цифры. Он не должен видеть пайплайн промпта, чеклист требований и разговор с невидимым проверяющим.
|
||||||
|
|
||||||
|
Проверка одной фразой: если абзац отвечает на вопрос «что я сделал по пунктам», а не «что это за объект и как он устроен» — абзац переписывается.
|
||||||
|
|
||||||
|
# 2. Как строится абзац
|
||||||
|
|
||||||
|
1. Тезис по существу.
|
||||||
|
2. Опора: число, факт, таблица, рисунок, конкретное свойство объекта.
|
||||||
|
3. Короткий вывод **про сам предмет**, если он действительно нужен.
|
||||||
|
|
||||||
|
Не начинай раздел преамбулой о том, зачем этот раздел существует. Сразу давай содержание.
|
||||||
|
|
||||||
|
Факт имеет право просто быть. Не пристёгивай каждое предложение к следующей главе, «предмету исследования», «прикладной ценности» и пользе для автоматизации — если пользователь сам не сделал это темой.
|
||||||
|
|
||||||
|
# 3. Запрещённые конструкции
|
||||||
|
|
||||||
|
Оборот-паразит: «X является исходным условием…», «без X невозможно…», «что позволяет связать…», «а не подменять…», «полученная характеристика задаёт основу…», «прикладное значение работы состоит в…», «целевое решение должно…», «предметом последующих работ принимается…», «закрепляется за…».
|
||||||
|
|
||||||
|
Мета-мораль в конце абзаца. Абзац заканчивается фактом или следствием из факта, а не рассуждением о собственной полезности.
|
||||||
|
|
||||||
|
Спор с невидимым рецензентом внутри основного текста. Комментарии к собственному методу вместо содержания.
|
||||||
|
|
||||||
|
Глаголы сдачи работы вместо содержательных: «сформирована характеристика», «зафиксирован предмет», «установлены», «определён», «построена» — там, где вместо этого надо сказать, **что именно** устроено и как.
|
||||||
|
|
||||||
|
# 4. Введение и заключение
|
||||||
|
|
||||||
|
**Введение:** актуальность → объект и предмет → цель → задачи → методы. Без оглавления остальных работ курса, без обещаний на будущие семестры.
|
||||||
|
|
||||||
|
**Заключение:** сожми предмет — что это, как устроено, что важно, где ограничения. Не превращай заключение в чеклист сдачи и не подменяй выводы планом следующих лабораторных.
|
||||||
|
|
||||||
|
Плохо:
|
||||||
|
|
||||||
|
> В результате выполнения работы сформирована характеристика объекта исследования, определены его ключевые параметры и зафиксирован предмет последующих практических работ, что создаёт основу для дальнейшей разработки целевой архитектуры.
|
||||||
|
|
||||||
|
Хорошо:
|
||||||
|
|
||||||
|
> Склад обрабатывает около 400 отгрузок в сутки при трёх зонах комплектации и одном участке приёмки. Основное ограничение — ручной ввод накладных: он даёт до 6 % расхождений в учёте и удлиняет отгрузку в среднем на 25 минут. Именно этот участок и определяет требования к системе.
|
||||||
|
|
||||||
|
# 5. Таблицы
|
||||||
|
|
||||||
|
Таблица описывает предмет, а не пользу таблицы. Последняя колонка — содержание, влияние или назначение в деятельности.
|
||||||
|
|
||||||
|
Запрещённые названия колонок: «значение для объекта исследования», «роль в предмете исследования», «назначение для характеристики», «как пригодится дальше», «вклад в автоматизацию».
|
||||||
|
|
||||||
|
Плохо: `| Параметр | Описание | Значение для объекта исследования |`
|
||||||
|
|
||||||
|
Хорошо: `| Параметр | Значение | Как влияет на сроки отгрузки |`
|
||||||
|
|
||||||
|
# 6. Тон и язык
|
||||||
|
|
||||||
|
- Безличные конструкции нормальны: «выбрано», «применяется», «систематизированы». Без «я» и «мы».
|
||||||
|
- Ссылки на источники — по делу, а не в каждом абзаце. Методички «как писать работу» в содержательную часть не тащи.
|
||||||
|
- Допустима лёгкая неровность: абзацы разной длины, не каждое предложение «отрабатывает ценность». Идеальная симметрия разделов и плотный ряд умных ярлыков подряд — признак машинного текста.
|
||||||
|
- Конкретика вместо консультантского лоска. «Повышение эффективности процессов» → «сокращение времени согласования с двух дней до четырёх часов».
|
||||||
|
- Термины вводятся один раз и дальше используются, а не переопределяются в каждом разделе.
|
||||||
|
|
||||||
|
# 7. Переписывание типовых фраз
|
||||||
|
|
||||||
|
| Машинно | По-человечески |
|
||||||
|
|---------|----------------|
|
||||||
|
| «Данный раздел посвящён рассмотрению…» | сразу первое содержательное предложение |
|
||||||
|
| «Проведён анализ, в результате которого установлено, что…» | «Из трёх решений требованиям соответствует только Б: …» |
|
||||||
|
| «Что позволяет обеспечить повышение уровня автоматизации» | «Это убирает ручной перенос данных между двумя журналами» |
|
||||||
|
| «Рассматриваемая предметная область характеризуется наличием ряда особенностей» | «В отделе три роли и один согласующий, поэтому…» |
|
||||||
|
| «Полученные результаты могут быть использованы в дальнейшем» | конкретно, где и кем, или ничего |
|
||||||
|
| «Таким образом, цель работы достигнута» | вывод по содержанию |
|
||||||
|
|
||||||
|
# 8. Самопроверка голоса
|
||||||
|
|
||||||
|
Перед выдачей проверь:
|
||||||
|
|
||||||
|
1. Ни один абзац не рассказывает о выполнении задания вместо предмета.
|
||||||
|
2. Ни один раздел не начинается преамбулой о своём назначении.
|
||||||
|
3. Нет запрещённых оборотов из п. 3 и колонок из п. 5.
|
||||||
|
4. Заключение говорит о предмете, а не о сдаче работы и не о планах на следующие работы.
|
||||||
|
5. Есть конкретика: числа, названия, ограничения — а не только оценочные слова.
|
||||||
|
6. Формат и разметка из основной инструкции соблюдены полностью.
|
||||||
@@ -1,317 +0,0 @@
|
|||||||
from docx import Document
|
|
||||||
from docx.shared import Pt, Cm, RGBColor
|
|
||||||
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING
|
|
||||||
from docx.enum.style import WD_STYLE_TYPE
|
|
||||||
from docx.oxml.ns import qn
|
|
||||||
from docx.oxml import OxmlElement
|
|
||||||
|
|
||||||
|
|
||||||
def create_gost_document(title="Документ", author="Автор"):
|
|
||||||
"""
|
|
||||||
Создает новый документ Word с оформлением по ГОСТ
|
|
||||||
|
|
||||||
Args:
|
|
||||||
title (str): Заголовок документа
|
|
||||||
author (str): Автор документа
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Document: Объект документа с настроенными стилями ГОСТ
|
|
||||||
"""
|
|
||||||
doc = Document()
|
|
||||||
|
|
||||||
# Настройка полей страницы согласно ГОСТ
|
|
||||||
sections = doc.sections
|
|
||||||
for section in sections:
|
|
||||||
section.top_margin = Cm(2) # Верхнее поле 2 см
|
|
||||||
section.bottom_margin = Cm(2) # Нижнее поле 2 см
|
|
||||||
section.left_margin = Cm(3) # Левое поле 3 см
|
|
||||||
section.right_margin = Cm(1.5) # Правое поле 1.5 см
|
|
||||||
|
|
||||||
# Установка стандартного стиля для всего документа
|
|
||||||
style = doc.styles['Normal']
|
|
||||||
font = style.font
|
|
||||||
font.name = 'Times New Roman'
|
|
||||||
font.size = Pt(14)
|
|
||||||
# Установка кириллического шрифта для документов на русском языке
|
|
||||||
font.element.rPr.rFonts.set(qn('w:eastAsia'), 'Times New Roman')
|
|
||||||
|
|
||||||
# Настройка формата абзаца
|
|
||||||
paragraph_format = style.paragraph_format
|
|
||||||
paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE # Полуторный интервал
|
|
||||||
paragraph_format.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY # Выравнивание по ширине
|
|
||||||
paragraph_format.first_line_indent = Cm(1.25) # Красная строка 1.25 см
|
|
||||||
|
|
||||||
# Создаем заголовок документа
|
|
||||||
title_paragraph = doc.add_heading(level=0)
|
|
||||||
title_paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
||||||
title_run = title_paragraph.add_run(title)
|
|
||||||
title_run.font.size = Pt(16)
|
|
||||||
title_run.font.bold = True
|
|
||||||
title_run.font.name = 'Times New Roman'
|
|
||||||
|
|
||||||
# Добавляем имя автора
|
|
||||||
author_paragraph = doc.add_paragraph()
|
|
||||||
author_paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
||||||
author_run = author_paragraph.add_run(f"\n{author}")
|
|
||||||
author_run.font.size = Pt(14)
|
|
||||||
author_run.font.name = 'Times New Roman'
|
|
||||||
|
|
||||||
# Добавляем отступ после заголовка
|
|
||||||
doc.add_paragraph()
|
|
||||||
|
|
||||||
# Создаем стили заголовков в соответствии с ГОСТ
|
|
||||||
create_gost_heading_styles(doc)
|
|
||||||
|
|
||||||
return doc
|
|
||||||
|
|
||||||
|
|
||||||
def create_gost_heading_styles(doc):
|
|
||||||
"""
|
|
||||||
Создает стили заголовков в соответствии с требованиями ГОСТ
|
|
||||||
|
|
||||||
Args:
|
|
||||||
doc (Document): Объект документа
|
|
||||||
"""
|
|
||||||
# Заголовок 1 уровня
|
|
||||||
h1_style = doc.styles.add_style('Heading 1 GOST', WD_STYLE_TYPE.PARAGRAPH)
|
|
||||||
h1_style.base_style = doc.styles['Heading 1']
|
|
||||||
h1_style.font.name = 'Times New Roman'
|
|
||||||
h1_style.font.size = Pt(16)
|
|
||||||
h1_style.font.bold = True
|
|
||||||
h1_style.font.all_caps = True # В ГОСТ заголовки часто пишутся прописными
|
|
||||||
h1_style.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
||||||
h1_style.paragraph_format.space_before = Pt(24) # Отступ перед заголовком
|
|
||||||
h1_style.paragraph_format.space_after = Pt(12) # Отступ после заголовка
|
|
||||||
h1_style.paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
|
||||||
|
|
||||||
# Заголовок 2 уровня
|
|
||||||
h2_style = doc.styles.add_style('Heading 2 GOST', WD_STYLE_TYPE.PARAGRAPH)
|
|
||||||
h2_style.base_style = doc.styles['Heading 2']
|
|
||||||
h2_style.font.name = 'Times New Roman'
|
|
||||||
h2_style.font.size = Pt(14)
|
|
||||||
h2_style.font.bold = True
|
|
||||||
h2_style.paragraph_format.space_before = Pt(24)
|
|
||||||
h2_style.paragraph_format.space_after = Pt(6)
|
|
||||||
h2_style.paragraph_format.first_line_indent = Cm(0) # Без красной строки
|
|
||||||
h2_style.paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
|
||||||
|
|
||||||
# Заголовок 3 уровня
|
|
||||||
h3_style = doc.styles.add_style('Heading 3 GOST', WD_STYLE_TYPE.PARAGRAPH)
|
|
||||||
h3_style.base_style = doc.styles['Heading 3']
|
|
||||||
h3_style.font.name = 'Times New Roman'
|
|
||||||
h3_style.font.size = Pt(14)
|
|
||||||
h3_style.font.bold = True
|
|
||||||
h3_style.font.italic = True
|
|
||||||
h3_style.paragraph_format.space_before = Pt(18)
|
|
||||||
h3_style.paragraph_format.space_after = Pt(6)
|
|
||||||
h3_style.paragraph_format.first_line_indent = Cm(0)
|
|
||||||
h3_style.paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
|
||||||
|
|
||||||
# Стиль для списка
|
|
||||||
list_style = doc.styles.add_style('List GOST', WD_STYLE_TYPE.PARAGRAPH)
|
|
||||||
list_style.base_style = doc.styles['List']
|
|
||||||
list_style.font.name = 'Times New Roman'
|
|
||||||
list_style.font.size = Pt(14)
|
|
||||||
list_style.paragraph_format.left_indent = Cm(1.25)
|
|
||||||
list_style.paragraph_format.space_before = Pt(0)
|
|
||||||
list_style.paragraph_format.space_after = Pt(6)
|
|
||||||
list_style.paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
|
||||||
|
|
||||||
# Стиль для подпись таблиц и рисунков
|
|
||||||
caption_style = doc.styles.add_style('Caption GOST', WD_STYLE_TYPE.PARAGRAPH)
|
|
||||||
caption_style.base_style = doc.styles['Caption']
|
|
||||||
caption_style.font.name = 'Times New Roman'
|
|
||||||
caption_style.font.size = Pt(12)
|
|
||||||
caption_style.font.italic = True
|
|
||||||
caption_style.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
||||||
caption_style.paragraph_format.space_before = Pt(6)
|
|
||||||
caption_style.paragraph_format.space_after = Pt(12)
|
|
||||||
|
|
||||||
|
|
||||||
def add_gost_heading(doc, text, level=1):
|
|
||||||
"""
|
|
||||||
Добавляет заголовок с оформлением по ГОСТ
|
|
||||||
|
|
||||||
Args:
|
|
||||||
doc (Document): Объект документа
|
|
||||||
text (str): Текст заголовка
|
|
||||||
level (int): Уровень заголовка (1, 2 или 3)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Paragraph: Объект параграфа с заголовком
|
|
||||||
"""
|
|
||||||
if level == 1:
|
|
||||||
style_name = 'Heading 1 GOST'
|
|
||||||
elif level == 2:
|
|
||||||
style_name = 'Heading 2 GOST'
|
|
||||||
elif level == 3:
|
|
||||||
style_name = 'Heading 3 GOST'
|
|
||||||
else:
|
|
||||||
raise ValueError("Уровень заголовка должен быть 1, 2 или 3")
|
|
||||||
|
|
||||||
# Проверяем, существует ли стиль, если нет - создаем стили ГОСТ
|
|
||||||
if style_name not in doc.styles:
|
|
||||||
create_gost_heading_styles(doc)
|
|
||||||
|
|
||||||
heading = doc.add_heading(text, level=0)
|
|
||||||
heading.style = style_name
|
|
||||||
return heading
|
|
||||||
|
|
||||||
|
|
||||||
def add_gost_paragraph(doc, text, style=None):
|
|
||||||
"""
|
|
||||||
Добавляет абзац с оформлением по ГОСТ
|
|
||||||
|
|
||||||
Args:
|
|
||||||
doc (Document): Объект документа
|
|
||||||
text (str): Текст абзаца
|
|
||||||
style (str, optional): Название стиля
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Paragraph: Объект параграфа
|
|
||||||
"""
|
|
||||||
p = doc.add_paragraph()
|
|
||||||
|
|
||||||
if style and style in doc.styles:
|
|
||||||
p.style = style
|
|
||||||
|
|
||||||
# Основной текст по ГОСТ
|
|
||||||
p.paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
|
||||||
p.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
|
|
||||||
p.paragraph_format.first_line_indent = Cm(1.25)
|
|
||||||
|
|
||||||
# Добавляем текст
|
|
||||||
p.add_run(text)
|
|
||||||
|
|
||||||
return p
|
|
||||||
|
|
||||||
|
|
||||||
def add_gost_table(doc, data, headers=None):
|
|
||||||
"""
|
|
||||||
Создает таблицу в формате ГОСТ
|
|
||||||
|
|
||||||
Args:
|
|
||||||
doc (Document): Объект документа
|
|
||||||
data (list): Данные таблицы в виде списка списков
|
|
||||||
headers (list, optional): Заголовки таблицы
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Table: Объект таблицы
|
|
||||||
"""
|
|
||||||
# Добавляем подпись таблицы
|
|
||||||
caption = doc.add_paragraph()
|
|
||||||
caption.style = 'Caption GOST'
|
|
||||||
caption.add_run("Таблица ").bold = True
|
|
||||||
|
|
||||||
# Создаем таблицу
|
|
||||||
rows = len(data) + (1 if headers else 0)
|
|
||||||
cols = len(data[0]) if data else 0
|
|
||||||
table = doc.add_table(rows=rows, cols=cols)
|
|
||||||
|
|
||||||
# Стиль таблицы
|
|
||||||
table.style = 'Table Grid'
|
|
||||||
|
|
||||||
# Заполняем заголовки, если они есть
|
|
||||||
if headers:
|
|
||||||
hdr_cells = table.rows[0].cells
|
|
||||||
for i, header in enumerate(headers):
|
|
||||||
hdr_cells[i].text = header
|
|
||||||
for paragraph in hdr_cells[i].paragraphs:
|
|
||||||
for run in paragraph.runs:
|
|
||||||
run.font.bold = True
|
|
||||||
|
|
||||||
# Заполняем данные
|
|
||||||
start_row = 1 if headers else 0
|
|
||||||
for i, row_data in enumerate(data):
|
|
||||||
row_cells = table.rows[i + start_row].cells
|
|
||||||
for j, cell_data in enumerate(row_data):
|
|
||||||
row_cells[j].text = str(cell_data)
|
|
||||||
|
|
||||||
# Настройка шрифта для всех ячеек
|
|
||||||
for row in table.rows:
|
|
||||||
for cell in row.cells:
|
|
||||||
for paragraph in cell.paragraphs:
|
|
||||||
for run in paragraph.runs:
|
|
||||||
run.font.name = 'Times New Roman'
|
|
||||||
run.font.size = Pt(12)
|
|
||||||
|
|
||||||
return table
|
|
||||||
|
|
||||||
|
|
||||||
def save_gost_document(doc, filename):
|
|
||||||
"""
|
|
||||||
Сохраняет документ с настройками совместимости для MS Word
|
|
||||||
|
|
||||||
Args:
|
|
||||||
doc (Document): Объект документа
|
|
||||||
filename (str): Имя файла для сохранения
|
|
||||||
"""
|
|
||||||
# Добавляем настройки совместимости
|
|
||||||
doc.settings.element.xpath('./w:compat')[0].clear()
|
|
||||||
|
|
||||||
# Сохраняем документ
|
|
||||||
doc.save(filename)
|
|
||||||
|
|
||||||
|
|
||||||
# Пример использования
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# Создаем документ по ГОСТ
|
|
||||||
document = create_gost_document("Отчет о научно-исследовательской работе", "Иванов И.И.")
|
|
||||||
|
|
||||||
# Добавляем раздел с заголовком 1 уровня
|
|
||||||
add_gost_heading(document, "ВВЕДЕНИЕ", level=1)
|
|
||||||
|
|
||||||
# Добавляем текст введение
|
|
||||||
intro_text = ("Настоящий отчет посвящен исследованию актуальных проблем в области "
|
|
||||||
"разработки программного обеспечения. Цель работы - анализ и разработка "
|
|
||||||
"методов оптимизации процесса создания программных продуктов. В ходе "
|
|
||||||
"исследования были применены методы системного анализа, математического "
|
|
||||||
"моделирования и экспериментальные методы.")
|
|
||||||
add_gost_paragraph(document, intro_text)
|
|
||||||
|
|
||||||
# Добавляем раздел с заголовком 2 уровня
|
|
||||||
add_gost_heading(document, "1. АНАЛИЗ ПРЕДМЕТНОЙ ОБЛАСТИ", level=2)
|
|
||||||
|
|
||||||
# Добавляем текст раздела
|
|
||||||
analysis_text = ("В данном разделе представлен анализ современных подходов к разработке "
|
|
||||||
"программного обеспечения. Проведенный анализ показал, что основными "
|
|
||||||
"проблемами являются недостаточная модульность архитектуры и слабая "
|
|
||||||
"документация проектов.")
|
|
||||||
add_gost_paragraph(document, analysis_text)
|
|
||||||
|
|
||||||
# Добавляем подраздел с заголовком 3 уровня
|
|
||||||
add_gost_heading(document, "1.1. Методы исследования", level=3)
|
|
||||||
|
|
||||||
# Добавляем нумерованный список
|
|
||||||
methods = [
|
|
||||||
"Анализ существующих решений в предметной области",
|
|
||||||
"Моделирование архитектуры программной системы",
|
|
||||||
"Экспериментальная проверка предложенных методов"
|
|
||||||
]
|
|
||||||
|
|
||||||
for method in methods:
|
|
||||||
p = document.add_paragraph(method, style='List GOST')
|
|
||||||
p.style = 'List GOST'
|
|
||||||
p.paragraph_format.left_indent = Cm(1.25)
|
|
||||||
p.paragraph_format.first_line_indent = Cm(-0.63) # Отрицательный отступ для номера
|
|
||||||
|
|
||||||
# Добавляем таблицу
|
|
||||||
headers = ["Метод", "Достоинства", "Недостатки"]
|
|
||||||
data = [
|
|
||||||
["Метод 1", "Высокая точность", "Сложность реализации"],
|
|
||||||
["Метод 2", "Простота", "Низкая точность"],
|
|
||||||
["Метод 3", "Универсальность", "Высокие требования к ресурсам"]
|
|
||||||
]
|
|
||||||
add_gost_table(document, data, headers)
|
|
||||||
|
|
||||||
# Добавляем заключение
|
|
||||||
add_gost_heading(document, "ЗАКЛЮЧЕНИЕ", level=1)
|
|
||||||
conclusion_text = ("В результате проведенного исследования были выявлены основные проблемы "
|
|
||||||
"в области разработки программного обеспечения. Предложенные методы "
|
|
||||||
"позволяют повысить качество создаваемых программных продуктов и сократить "
|
|
||||||
"время на их разработку.")
|
|
||||||
add_gost_paragraph(document, conclusion_text)
|
|
||||||
|
|
||||||
# Сохраняем документ
|
|
||||||
save_gost_document(document, "otchet_po_GOST.docx")
|
|
||||||
print("Документ успешно создан и сохранен как 'otchet_po_GOST.docx'")
|
|
||||||
+186
@@ -0,0 +1,186 @@
|
|||||||
|
# Незакрытые задачи после аудита
|
||||||
|
|
||||||
|
Состояние на 10 сентября 2026. Всё перечисленное — то, что осталось после прохода по плану оптимизации. Что уже сделано, см. [audit-report.md](audit-report.md).
|
||||||
|
|
||||||
|
Тесты сейчас: **320 passed, 1 skipped, 174 subtests**, полный прогон 11.85 с. Ничего не закоммичено — всё лежит в staging и рабочем дереве.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P0 — сломает сборку, если не сделать
|
||||||
|
|
||||||
|
### `mdexport/` не добавлен в git
|
||||||
|
|
||||||
|
Пакет `mdexport/` (общий обходчик AST для `md2latex` и `md2fodt`) создан, но остался **untracked**, при этом на него уже завязаны:
|
||||||
|
|
||||||
|
- `pyproject.toml` — перечислен в `[tool.hatch.build.targets.wheel].packages`
|
||||||
|
- `md2latex/emitter.py`, `md2latex/escape.py`
|
||||||
|
- `md2fodt/emitter.py`, `md2fodt/escape.py`
|
||||||
|
|
||||||
|
На свежем клоне оба конвертера упадут с `ModuleNotFoundError`, сборка колеса тоже. Локально работает только потому, что файлы лежат на диске.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git add mdexport/blocks.py mdexport/inline.py mdexport/labels.py mdexport/__init__.py
|
||||||
|
git status --short mdexport
|
||||||
|
```
|
||||||
|
|
||||||
|
Скомпилированные `.pyc` внутри уже покрыты правилом `__pycache__/`, добавлять их не нужно.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P1 — найденный баг, теряет данные
|
||||||
|
|
||||||
|
### `word2md` выбрасывает пункты списка при смене его типа
|
||||||
|
|
||||||
|
Найден при разборе `_handle_paragraph`. Баг **существовал и до рефакторинга**, его сохранили намеренно, чтобы правка осталась поведенчески нейтральной.
|
||||||
|
|
||||||
|
`word2md/walker.py`, строки 406-429. Оба обработчика списков заканчиваются на `return []`, выбрасывая накопленный `out` — притом что `_flush_code(out)` и `_flush_list(out)` внутри уже успели в него дописать:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _as_numbered_list_item(self, para: _Para, out: list[Block]) -> list[Block] | None:
|
||||||
|
if not paragraph_has_numpr(para.paragraph):
|
||||||
|
return None
|
||||||
|
self._flush_code(out) # может дописать в out
|
||||||
|
...
|
||||||
|
self._append_list_item(ordered, body, out) # внутри вызывает _flush_list(out)
|
||||||
|
return [] # <- out выброшен
|
||||||
|
```
|
||||||
|
|
||||||
|
`_append_list_item` (строки 431-437) при смене типа списка вызывает `_flush_list(out)`, который сбрасывает накопленные пункты в `out`. После чего `return []` их теряет. Соседний `_as_plain_paragraph` (строка 439) в такой же ситуации корректно делает `return out`.
|
||||||
|
|
||||||
|
Воспроизведение: два упорядоченных пункта с `numPr`, за ними пункт-буллет. Оба упорядоченных пропадают из результата. Тот же механизм теряет и забуференный код, если он был.
|
||||||
|
|
||||||
|
Порядок работ: сначала красный тест на исчезающие пункты, потом замена `return []` на `return out` в обоих обработчиках, потом проверка, что побайтовая сверка вывода на остальных документах не поехала (она поедет только там, где баг срабатывал).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P1 — решено, но не выполнено
|
||||||
|
|
||||||
|
### Калибровка Courier New 10 пт
|
||||||
|
|
||||||
|
Ты выбрал вариант «10 пт канон по табл. 4.1, добавить калибровку и перевести тест-фикстуру». **Не сделано** — я не успел запустить задачу до конца сессии.
|
||||||
|
|
||||||
|
Что именно нужно, в `md2gost/renderable/paragraph_sizer.py`, метод `Font.get_line_height` (строки 78-83):
|
||||||
|
|
||||||
|
```python
|
||||||
|
# TODO: make it work for all fonts
|
||||||
|
if "Times" in str(self._face.family_name) and self._freetypefont.size == 14:
|
||||||
|
return Pt(16.05)
|
||||||
|
if "Courier" in str(self._face.family_name) and self._freetypefont.size == 12:
|
||||||
|
return Pt(13.61)
|
||||||
|
```
|
||||||
|
|
||||||
|
Сейчас калиброванная ветка есть только для Courier 12 пт. Замеры, снятые в ходе аудита:
|
||||||
|
|
||||||
|
| Размер | Возвращает сейчас | Фактическая высота |
|
||||||
|
|---|---|---|
|
||||||
|
| Courier 10 пт | 11.000 | ≈ 11.34 |
|
||||||
|
| Courier 11 пт | 12.000 | — |
|
||||||
|
| Courier 12 пт | 13.610 (калибровка) | 13.61 |
|
||||||
|
| Courier 14 пт | 16.000 | — |
|
||||||
|
|
||||||
|
Целые числа выдаёт округление FreeType; калибровочные ветки существуют ровно затем, чтобы подменить округление реальным значением Word. Пропорция от 13.61 при переходе 12 → 10 пт даёт примерно 11.34.
|
||||||
|
|
||||||
|
Итог: пока ветки для 10 пт нет, **каждая строка листинга меряется примерно на 3% короче**, и на длинных листингах разъезжаются разрывы страниц.
|
||||||
|
|
||||||
|
Порядок работ:
|
||||||
|
|
||||||
|
1. Добавить ветку `Courier` + размер 10 → `Pt(11.34)`. Лучше сначала подтвердить число замером в Word через COM, а не принимать пропорцию на веру.
|
||||||
|
2. Перевести `tests/__init__.py` (строка 27) с `Pt(12)` на `Pt(10)`, чтобы фикстура совпадала с `style_config.py:305` и табл. 4.1.
|
||||||
|
3. Прогнать `tests/test_paragraph_sizer.py` — серия `test_count_lines_courier1..10` завязана на текущую высоту, ожидаемые значения поедут.
|
||||||
|
|
||||||
|
Не делай пункт 2 без пункта 1 — иначе калиброванная ветка перестанет срабатывать, и замер станет хуже, чем сейчас.
|
||||||
|
|
||||||
|
### `Other_Code/` — 59 файлов, 1158 КБ
|
||||||
|
|
||||||
|
Это была Фаза 4.2 плана, и я её просто не назначил ни одному исполнителю при раскладке работ. Каталог до сих пор полностью в git.
|
||||||
|
|
||||||
|
Ни одного импорта из `md2gost`, `word2md`, `md2latex`, `md2fodt`, `tests/`; не упоминается в `.spec`, `.bat`, `.github/workflows/`, `pyproject.toml`. Единственные ссылки — раздел 12 в `PROJECT.md` и внутренние перекрёстные ссылки самого каталога.
|
||||||
|
|
||||||
|
Команды и три варианта действий расписаны в [git-cleanup.md](git-cleanup.md), раздел 1.
|
||||||
|
|
||||||
|
### Личные документы в корне
|
||||||
|
|
||||||
|
`ТарасовИА_КР_v3.docx` (2326 КБ) и `Практическая_работа_6.docx` (42 КБ) — ФИО прямо в имени файла. Ты сказал, что уберёшь руками; команды и предупреждение про ловушку с `md2gost/Template.docx` в [git-cleanup.md](git-cleanup.md), раздел 2.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P2 — открытые вопросы по методичке
|
||||||
|
|
||||||
|
### «С новой страницы» для надписи листинга
|
||||||
|
|
||||||
|
Табл. 4.1 (`docs/reference/method-mirea-2022.txt`, строки 714-715) в блоке «Положение на странице» перечисляет и «Не отрывать от следующего», и «С новой страницы».
|
||||||
|
|
||||||
|
В `md2gost/style_config.py` у стиля `Caption Listing` (строки 278-291) есть `keep_with_next=True`, но нет `page_break_before=True`. Вопрос не праздный: в репозитории есть целая система режимов `--listing-continuation`, так что отсутствие разрыва может быть осознанным выбором. Плюс `Caption.__init__` теперь явно ставит `page_break_before = False`, чтобы не плодить пустые страницы при сочетании с `keep_with_next` — то есть менять придётся согласованно в двух местах.
|
||||||
|
|
||||||
|
Решение нужно принять до того, как кто-то «починит» это по табл. 4.1 в лоб.
|
||||||
|
|
||||||
|
### Правило `heading.empty` ложно срабатывает внутри листингов
|
||||||
|
|
||||||
|
Новое правило в `md2gost/checker/__init__.py` не вырезает fenced-блоки, поэтому одинокая строка `#` внутри блока кода будет засчитана как пустой заголовок. Поведение консистентно с уже существующим `HEADING_RE` в том же файле, то есть это общая для checker'а проблема, а не привнесённая. Чинить имеет смысл сразу для всех проверок заголовков.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P3 — не начатое из Фазы 6
|
||||||
|
|
||||||
|
Всё это помечено в плане как опциональное. Ничего из перечисленного не сделано, кроме общего обходчика AST и разбора `word2md/_handle_paragraph`.
|
||||||
|
|
||||||
|
### Разбиение `idef0.py`
|
||||||
|
|
||||||
|
1935 строк (сейчас 74 КБ). God-функции: `_parse_arrow_line` (~177 строк), `_draw_kit_form` (~121), `_route_icom` (~110), `_draw_arrow` (~83), `_layout_arrow_label` (~80).
|
||||||
|
|
||||||
|
Предлагаемая раскладка в пакет `idef0/`: `model.py`, `parse.py`, `layout.py`, `routing.py`, `scene.py`, `draw.py`, `render.py`, с реэкспортом публичного API из `__init__.py` ради тестов. Выигрыша в скорости не даёт, только читаемость.
|
||||||
|
|
||||||
|
Отдельно там же: `_Scene.to_png()` и `_Scene.to_svg()` дважды обходят один и тот же список операций. Можно сгенерировать SVG и растеризовать его в PNG.
|
||||||
|
|
||||||
|
### Разбиение `gui.py`
|
||||||
|
|
||||||
|
1695 строк (82 КБ) в одном классе `Md2GostApp`: главное окно, шесть диалогов, редактор UML-схем на ~270 строк, композер промптов, логика перезаписи файлов, потоки конвертации, загрузка ассетов.
|
||||||
|
|
||||||
|
Раскладка: `gui/windows/`, `gui/widgets.py`, `gui/convert_runner.py`, а `Md2GostApp` оставить тонкой оболочкой.
|
||||||
|
|
||||||
|
### Ленивые импорты
|
||||||
|
|
||||||
|
`md2gost/pipeline.py` (строки 15-19) и `md2gost/gui.py` (строки 11-32) тянут весь конвейер на старте: `docx`, pygments, PIL, lxml. `python -m md2gost --help` платит за это полную цену. Matplotlib и playwright уже ленивые — сделать так же с остальным.
|
||||||
|
|
||||||
|
### Общий сборщик `ConvertRequest`
|
||||||
|
|
||||||
|
`gui.py` (строки 1730-1760) и `__main__.py` (строки 272-317) дублируют маппинг примерно двадцати полей и уже разошлись: дефолт типа документа в GUI `coursework`, в CLI `practice`.
|
||||||
|
|
||||||
|
### Флаг `--open`
|
||||||
|
|
||||||
|
`__main__.py` строки 315-316: `open_when_done=bool(args.debug)`. Открытие готового файла привязано к дебагу, отдельного флага нет, хотя в GUI есть галочка.
|
||||||
|
|
||||||
|
### Скачивание PlantUML блокирует Tk
|
||||||
|
|
||||||
|
`gui.py` строка 1537 — синхронный вызов `fetch_plantuml_jar`, тогда как Graphviz (1576) и Chromium (1622) рядом уже качаются в отдельном потоке.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P3 — документация не догоняет код
|
||||||
|
|
||||||
|
### Таблицы модулей в `PROJECT.md` не знают про `mdexport`
|
||||||
|
|
||||||
|
Новый пакет `mdexport` не попал в разделы с картой репозитория и перечнем модулей — исполнитель не владел этим файлом и намеренно его не трогал. Заодно стоит убрать оттуда упоминания удалённых прототипов, если раздел 12 их ещё перечисляет.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P4 — замечания исполнителей, оставленные как есть
|
||||||
|
|
||||||
|
- **`uv.lock` в порядке.** `uv lock` отработал с сетью, добавил matplotlib 3.11.1 с транзитивными зависимостями, `uv lock --check` проходит. Лок синхронен и по matplotlib, и по `pywin32` — пересобирать не нужно.
|
||||||
|
- **Playwright закрывается в конце `convert()`.** Это и было целью Фазы 3.4, но у GUI-сценария появилась плата: при каждой следующей конвертации браузер поднимается заново вместо переиспользования. Если mermaid-диаграммы конвертируются часто, стоит подумать про пул с таймаутом.
|
||||||
|
- **Кастомный `--plantuml-jar` другой версии не отражается в ключе кэша.** Префикс использует прибитую константу `PLANTUML_VERSION`. Фингерпринт самого файла закрыл бы дыру, но заставлял бы кэш протухать при каждом появлении и исчезновении jar.
|
||||||
|
- **Версия jar в ключе кэша задевает и Kroki-рендеры**, потому что не-mermaid типы делят один путь кэша между бэкендами. Бамп версии обесценит и записи, отрисованные через Kroki. Это лишняя перерисовка, не более.
|
||||||
|
- **Если одна диаграмма роняет батч-JVM, отбрасывается вся пачка** и каждая диаграмма уходит на свою JVM. Корректно, но стоит одной впустую потраченной JVM.
|
||||||
|
- **`--syntax-highlighting --strict --check` теперь завершается с кодом 1**, потому что новое правило `listing.syntax_highlighting` под `--strict` становится ошибкой. Это ровно то поведение, которое закладывалось, но про него надо помнить в скриптах.
|
||||||
|
- **Playwright изредка печатает `Task was destroyed but it is pending`** на выходе интерпретера при большом комбинированном прогоне. Не воспроизвелось повторно, все тесты проходят, тот же путь закрытия вызывался и до правок.
|
||||||
|
- **Правила игнора `/tmp_*.png` и `/tmp_*.puml`** захватили рабочие черновики `tmp_org_full.*`, `tmp_org_uml.*`, `tmp_arch.*`. Файлы на диске целы, но из `git status` пропали.
|
||||||
|
- **`todo.txt` в корне** остался нетронутым — это твой личный блокнот, я его не трогал и не сливал с этим файлом.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Что стоит сделать перед коммитом
|
||||||
|
|
||||||
|
1. `git add mdexport/` — иначе всё сломается на свежем клоне.
|
||||||
|
2. Просмотреть `git diff --cached` и `git diff` — там 34 файла изменений плюс 8095 удалённых строк.
|
||||||
|
3. Прогнать `.venv\Scripts\python.exe -m pytest -q` ещё раз после `git add`.
|
||||||
|
4. Собрать exe одним из `build-exe*.bat` — ни один из билд-скриптов в ходе аудита не проверялся, а `pyproject.toml` и состав пакетов поменялись.
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
# Отчёт по аудиту и оптимизации
|
||||||
|
|
||||||
|
Проведён 9-10 сентября 2026. Незакрытые задачи — в [TODO.md](TODO.md), команды ручной чистки git — в [git-cleanup.md](git-cleanup.md).
|
||||||
|
|
||||||
|
**Ничего не закоммичено.** Всё лежит в staging и рабочем дереве под ревью.
|
||||||
|
|
||||||
|
## Итог одной таблицей
|
||||||
|
|
||||||
|
| | Было | Стало |
|
||||||
|
|---|---|---|
|
||||||
|
| Замер страницы из 5 абзацев | 80.06 мс | 18.08 мс |
|
||||||
|
| Конструирований `Font` за проход | 19 | 0 |
|
||||||
|
| Вызовов `get_text_width` за проход | 814 | 114 |
|
||||||
|
| Конвертация формулы | 13.29 мс | 0.66 мс |
|
||||||
|
| PlantUML, 4 диаграммы, PNG | 4.99 с | 1.25 с |
|
||||||
|
| PlantUML, 4 диаграммы, SVG | 9.02 с | 2.33 с |
|
||||||
|
| Запусков JVM на документ | N (2N в SVG) | 1 (2 в SVG) |
|
||||||
|
| Сквозная конвертация документа | — | на 10-23% быстрее |
|
||||||
|
| Тесты | 54 passed | 320 passed, 174 subtests |
|
||||||
|
|
||||||
|
Объём изменений: 34 файла правок (+1552 / −698), плюс 34 файла удалений (−8095 строк).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Исправлено три бага вывода
|
||||||
|
|
||||||
|
Работа шла строго по TDD: сначала три падающих теста, прогон с 9 падениями по ожидаемым причинам, затем правки. Каждый фикс привязан к пункту методички МИРЭА (`docs/reference/method-mirea-2022.txt`).
|
||||||
|
|
||||||
|
### Гиперссылка дублировала свой текст
|
||||||
|
|
||||||
|
`md2gost/renderable/paragraph.py`, строка 44. В `Link.add_run` цикл по сегментам присваивал `docx_run.text = text` вместо `part`, поэтому ссылка с дефисом выводилась полным текстом столько раз, сколько в ней сегментов.
|
||||||
|
|
||||||
|
Красный тест показал это нагляднее всего на интервале: `['100', '150'] != ['100-150', '100-150']`. На сложном слове: `'вопервых' != 'во-первыхво-первых'`.
|
||||||
|
|
||||||
|
Опечатка при копировании из `Paragraph.add_run`, где на строке 104 тот же цикл написан правильно.
|
||||||
|
|
||||||
|
Обоснование: строки 373-376 методички — дефис не отделяется пробелами и используется в сложных словах («во-первых», «физико-математический») и интервалах («100-150 Мб»), поэтому слово не должно рваться при переносе. Ради этого и вставляется отдельный элемент `w:noBreakHyphen` вместо текстового дефиса.
|
||||||
|
|
||||||
|
### Курсив в подсветке синтаксиса не доезжал до XML
|
||||||
|
|
||||||
|
`md2gost/renderable/listing.py`, строка 36. Выражение `style["italic"] in style or None` проверяло, есть ли значение `True`/`False` среди **ключей** словаря стиля. Всегда ложно, курсив не применялся никогда.
|
||||||
|
|
||||||
|
Здесь пришлось разбираться, что вообще считать правильным поведением. Табл. 4.1 методички (строки 716-721) требует для содержимого листинга `Courier New / Начертание Обычный / 10 пт / Цвет Авто (чёрный)`. Стили в `style_config.py:303-314` этому соответствуют, но `DocxParagraphPygmentsFormatter` перекрывает их на уровне run'а, навешивая полужирный и цвет. То есть подсветка как таковая противоречит методичке, а сломанный курсив лишь случайно оказывался ближе к требованию.
|
||||||
|
|
||||||
|
Решение в два шага: выражение исправлено на `style.get("italic") or None`, чтобы флаг делал то, что обещает, и добавлено правило `listing.syntax_highlighting` в checker — предупреждение по умолчанию, ошибка под `--strict`. Флаг выключен по умолчанию, так что это осознанное отклонение, но пользователь узнаёт о нём из отчёта, а не из замечаний преподавателя.
|
||||||
|
|
||||||
|
Заодно добавлен регрессионный страж: при выключенном флаге ни один run в стиле `Code` не должен задавать `w:b`, `w:i` или `w:color`.
|
||||||
|
|
||||||
|
### Пустой заголовок ронял парсер
|
||||||
|
|
||||||
|
`md2gost/extended_markdown/heading.py`, строка 23. `inline_body[0] == "*"` давал `IndexError` на строке `#` без текста. Заменено на `startswith`.
|
||||||
|
|
||||||
|
Плюс правило `heading.empty` в checker'е: пустой заголовок дал бы пронумерованный безымянный раздел в автособираемом содержании. Обоснование — строка 156 методички: «разделы, подразделы, пункты имеют заголовки, четко и кратко отражающие их содержание».
|
||||||
|
|
||||||
|
Побочно выяснилось расхождение в постановке: я просил проверить форму `#*`, но такая строка регуляркой заголовка вообще не матчится — после `#` обязателен пробел или конец строки. Заголовком является только `# *`, эта форма и покрыта тестом.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Горячий путь измерения текста ускорен в 4.4 раза
|
||||||
|
|
||||||
|
### Кэш объектов `Font`
|
||||||
|
|
||||||
|
Замер показал, что один `Font()` конструируется 2.62 мс: открывает FreeType `Face`, грузит `ImageFont.truetype` и аллоцирует PIL-канвас 1000×1000. При этом он создавался дважды за вызов `calculate_height` плюс по разу на каждый run.
|
||||||
|
|
||||||
|
Добавлен модульный `_get_font(name, bold, italic, size_pt)` под `lru_cache(maxsize=256)`, все три места конструирования переведены на него. Сам класс `Font` оставлен публичным — существующие тесты создают его напрямую.
|
||||||
|
|
||||||
|
### Скрытая мина в `is_mono`
|
||||||
|
|
||||||
|
Попутно нашлось то, чего в плане не было. Ветка `get_text_width` для моноширинных шрифтов читала `self._face.glyph.advance.x` — то есть тот глиф, который последним оставил в слоте `load_char`. Всегда оказывался `"m"`, потому что единственные вызовы `load_char` живут в `is_mono`. Значение было верным, но верным по случайности: любое постороннее обращение к face молча его сломало бы.
|
||||||
|
|
||||||
|
`is_mono` заменён на свойство `_metrics`, которое снимает и флаг моноширинности, и advance за один проход. Результат теперь не зависит от порядка обращений к слоту глифа. Есть тест, который долбит `load_char` посторонними символами и проверяет, что ширина не поехала.
|
||||||
|
|
||||||
|
### Квадратичный пересчёт ширины слова
|
||||||
|
|
||||||
|
`count_lines` пересчитывал ширину префикса слова после каждого символа, хотя результат читается только в ветке `if c == " "`. Вызов перенесён в две точки: на пробеле и один раз в конце каждого run.
|
||||||
|
|
||||||
|
Вторая точка — та, которую легко пропустить: слово может пересекать границу run'ов, а следующая итерация добавляет свежий слот в `word_parts_widths`, поэтому фрагмент нужно померить до этого.
|
||||||
|
|
||||||
|
Намеренно сохранено одно неочевидное поведение оригинала: `word_part` не сбрасывается после замера на пробеле, потому что исходный код сбрасывал его только внутри ветки `if any(word_parts_widths)`, и фрагмент нулевой ширины продолжал накапливаться через пробел.
|
||||||
|
|
||||||
|
### Синглтон XSLT
|
||||||
|
|
||||||
|
`md2gost/latex_math.py` парсил `mml2omml` с диска и компилировал `etree.XSLT` заново на каждую формулу. Вынесено в ленивый `_mml2omml_transform()`. Оставлено внутри `try`, чтобы битый или отсутствующий stylesheet по-прежнему давал тот же `ValueError` при каждом вызове.
|
||||||
|
|
||||||
|
### Как доказана эквивалентность
|
||||||
|
|
||||||
|
Не чтением кода. Пять эталонных тестовых файлов прогнаны до и после, verbose-вывод сдиффен построчно — идентичен. Плюс `tests/test_paragraph_sizer_cache.py` содержит дословные копии дооптимизационных `Font` и `count_lines` и сверяет с ними новый код на 158 случаях: одиночные и многорановые абзацы, разрыв слова на границе run'а, пустые run'ы, `w:noBreakHyphen`, смешанные полужирный с курсивом, кириллица, длинные неразрывные слова, сдвоенные и хвостовые пробелы, три ширины колонки, оба значения `is_mono`.
|
||||||
|
|
||||||
|
Чтобы убедиться, что корпус не вырожденный, замер в конце run'а был временно отключён — тесты упали в 5 местах.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Диаграммы: одна JVM на документ вместо одной на диаграмму
|
||||||
|
|
||||||
|
Перед проектированием было проверено фактическое поведение jar: PlantUML принимает много входных файлов за запуск, но только один флаг `-t<fmt>` (передача `-tpng -tsvg` молча отдаёт только SVG), а относительный `-o` резолвится от каталога каждого входного файла.
|
||||||
|
|
||||||
|
Отсюда конструкция: каждый источник получает свой подкаталог `NNNN/src.puml` с `-o out`. Это заодно решает коллизию двух источников, объявляющих `@startuml SameName`.
|
||||||
|
|
||||||
|
`DiagramFigure.__init__` регистрирует свой блок через `register_pending_diagram()`. Поскольку `Converter.convert()` полностью материализует `list(self.parser.parse())` до рендера, к моменту первого вызова `render_diagram()` очередь содержит весь документ. Первый вызов запускает одноразовый предпроход: готовит все источники, отбрасывает попавшие в кэш, остальные рендерит одной JVM.
|
||||||
|
|
||||||
|
Всё, что упало в предпроходе, остаётся промахом кэша и идёт обычным путём, так что цепочка PlantUML jar → локальный Kroki → удалённый Kroki и режимы `--diagram-fallback` не тронуты. `_render_plantuml_jar_one` теперь обёртка на одну задачу поверх того же батч-раннера, чтобы одиночный и пакетный пути не разъехались.
|
||||||
|
|
||||||
|
### Найденный попутно баг
|
||||||
|
|
||||||
|
Старый код писал `diagram.puml` и искал `diagram.{fmt}`. Любой источник с `@startuml SomeName` производил `SomeName.png`, считался провалом jar и молча уезжал на Kroki. Новая логика забирает то, что jar реально положил в выходной каталог этой диаграммы, и рендерит локально.
|
||||||
|
|
||||||
|
### Остальное по диаграммам
|
||||||
|
|
||||||
|
- Версия jar добавлена в ключ кэша: `PLANTUML_CACHE_PREFIX = f"plantuml-{PLANTUML_VERSION}\n"`. Раньше для plantuml префикс был пустой, и обновление jar отдавало старые PNG.
|
||||||
|
- `ensure_user_schemes()` убран из `render_diagram` — он мигрировал `md2gost.schemes.json` на каждую диаграмму. Безопасно: `load_merged_schemes()` его и так вызывает.
|
||||||
|
- Добавлен `shutdown_diagrams()`, закрывающий сессию Playwright, вызывается из существующего `finally` в `pipeline.convert()`.
|
||||||
|
- Graphviz для DFD теперь отдаёт PNG и SVG из одного процесса `dot`. Проверяет наличие и непустоту каждого выхода и повторяет недостающий формат старым путём, если сборка Graphviz не поддерживает пары.
|
||||||
|
|
||||||
|
Что проверено вживую: путь через jar целиком, включая батч, попадания в кэш, источники с `@startuml Name`, полный прогон `pipeline.convert()`. Graphviz тоже оказался установлен — комбинированный запуск реально произвёл оба файла, и на этом всплыло, что существующий DFD-тест тянулся мимо своего мока.
|
||||||
|
|
||||||
|
Что не проверено: локальный и удалённый Kroki (сервера нет, сетевых вызовов не делалось) — только моки; закрытие браузера в реальной mermaid-конвертации, потому что здешние тесты идут через QuickJS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Двойная работа при рендере убрана
|
||||||
|
|
||||||
|
Все три места сначала обвешаны замерами, потом правлены. Доказательство для каждого — матрица из 4 документов на 7 наборов опций, 28 конвертаций, и побайтовое сравнение `word/document.xml` со снимком, снятым до любых правок. **28 из 28 идентичны.** В матрице `examples/example.md`, `examples/pis_custom.md`, синтетический стресс-документ (многостраничная дробящаяся таблица со слитыми ячейками, многостраничный листинг, три фигуры) и документ с альбомной ориентацией, каждый в режимах продолжений off / legacy / caption / soft, плюс ручное содержание, тип `PIS_custom` и авто-нумерация заголовков.
|
||||||
|
|
||||||
|
### Повторный рендер на границе страницы
|
||||||
|
|
||||||
|
`md2gost/renderer.py`. Раньше: отрендерить блок, померить первый `RenderedInfo`, и если не влезает — выбросить генератор и вызвать `render()` заново. Для таблицы или листинга это пересборка всего поддерева DOCX и повтор всех замеров.
|
||||||
|
|
||||||
|
Решение оказалось точнее, чем «разделить measure и emit», и это выяснилось именно замером. Повтор нужен только когда в `self._to_new_page` реально что-то стоит в очереди: отложенные фигуры вставляются первыми и двигают границу страницы, поэтому блок приходится раскладывать заново уже против оставленного ими состояния. Если очередь пуста, `_flush_to_new_screen()` — no-op, и повторный рендер строит то же поддерево против того же состояния.
|
||||||
|
|
||||||
|
Инструментирование показало, что **во всех срабатываниях на всей матрице очередь была пуста**, то есть второй проход был чистой потерей всегда. Триггером почти везде оказывались заголовки с `page_break_before` и одна фигура — ни разу таблица или листинг.
|
||||||
|
|
||||||
|
Условие дополнено проверкой `and self._to_new_page`. Структура render → measure → flush → re-render сохранена ровно для того случая, когда flush действительно меняет состояние.
|
||||||
|
|
||||||
|
### Подпись рисунка мерилась дважды
|
||||||
|
|
||||||
|
`md2gost/renderable/image.py`. Предложенный в плане `Image.estimate_height()` **не понадобился**: замер показал, что геометрия считается дёшево, а реальная стоимость сухого прохода — построение свежего `Caption` и прогон сайзера по нему. Поэтому подпись создаётся один раз и переиспользуется (сбрасывается в `set_number()`), а сама структура «сухой проход плюс настоящий рендер» не тронута. Именно поэтому вывод и смог остаться идентичным.
|
||||||
|
|
||||||
|
### Кэш замеров абзаца
|
||||||
|
|
||||||
|
Строки таблицы мерились до трёх раз. Теперь `_peek_row_height` и `_build_docx_row` идут через один мемоизированный замер, а выбрасываемый сухой `Caption` в `_start_block_height` убран — `render()` строит подпись один раз и передаёт её внутрь.
|
||||||
|
|
||||||
|
Общий хелпер `measure_paragraph()` в `paragraph_sizer.py` мемоизирует `calculate_height()` в разрезе владеющего renderable. Ключ собран так, чтобы это было безопасно в коде, решающем разрывы страниц:
|
||||||
|
|
||||||
|
- сериализованный XML измеряемого абзаца, а **не** идентичность объекта — правка между двумя вызовами тогда даёт промах кэша вместо устаревшей высоты;
|
||||||
|
- сериализованный `w:pPr` предшественника, а не он целиком — сайзер читает только `pPr`, а привязка `w:p` к телу документа заставляет lxml поднять на него объявления пространств имён, из-за чего полная сериализация выглядела бы как правка и молча промахивалась;
|
||||||
|
- ширина.
|
||||||
|
|
||||||
|
Сверх побайтового сравнения был прогнан «паранойяльный» режим, пересчитывающий значение на каждом попадании в кэш и сверяющий с сохранённым. Вся матрица прошла без единого устаревшего попадания. Строительные леса убраны, а `tests/test_paragraph_sizer_cache.py` получил семь регрессионных тестов: попадание равно свежему расчёту, инвалидация при правке текста и при правке интервалов, ширина и интервалы предшественника как части ключа, контракт приватной копии (вызывающий обнуляет `.before` на новой странице), случай с привязкой предшественника.
|
||||||
|
|
||||||
|
Попутно `page_break_before = False` перенесён из конца `render()` в `__init__`, чтобы оба замера подписи видели один и тот же абзац.
|
||||||
|
|
||||||
|
**Сквозное время конвертации упало на 10-23%** в зависимости от документа.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Удалено 6777 строк мёртвого кода
|
||||||
|
|
||||||
|
Больше моей исходной оценки в 5646. Для каждого файла искались импорты модуля, все top-level символы (извлечены через `ast`) и имя файла в `*.spec`, `*.bat`, `*.toml`, `.github/workflows/`, `tests/` и всех живых пакетах.
|
||||||
|
|
||||||
|
| Файл | Строк | Чем был |
|
||||||
|
|---|---|---|
|
||||||
|
| `mm3.py` | 2210 | Монолит + matplotlib-графики |
|
||||||
|
| `mm2.py` | 1551 | Монолитный сборщик DOCX, версия 2 |
|
||||||
|
| `main12.py` | 915 | Ранний `MarkdownGostDocument` |
|
||||||
|
| `font.py` | 860 | Своя реализация Font API |
|
||||||
|
| `parser.py` | 340 | Песочница на mistune |
|
||||||
|
| `asd.py` | 317 | Эксперимент со стилями ГОСТ |
|
||||||
|
| `mm5.py` | 284 | Прототип на mistune |
|
||||||
|
| `mm4.py` | 241 | Построчный прототип |
|
||||||
|
| `main.py` | 32 | Hello-world на python-docx |
|
||||||
|
| `_audit_tmp.py` | 27 | Разовый инспектор DOCX |
|
||||||
|
|
||||||
|
Проверены две ловушки: корневой `parser.py` мёртв, а `md2gost/parser_.py` живой и импортируется в `converter.py`; корневой `font.py` мёртв, а `md2gost/renderable/find_font.py` живой. Единственное вхождение `font` в импортах живых пакетов — `from docx.text.font import Font as DocxFont`, то есть сторонняя библиотека.
|
||||||
|
|
||||||
|
Мёртвый код внутри пакета: убраны `APPENDIX_RE` в `renderer.py` (дублировал константу из `label_pass.py`), `Listing._create_table`, `Converter._output_path`, no-op ветка `isinstance(r, Paragraph): pass` в `label_pass.py`, `_BrowserSession.render_svg()`, `_draw_purpose()` в `idef0.py`, плюс осиротевший `import os` в `paragraph_sizer.py`.
|
||||||
|
|
||||||
|
Две поправки к моему списку. **`gui.main()` оказался живым** — это консольный скрипт `md2gost-gui` в `pyproject.toml`, я ошибочно записал его в мёртвые; оставлен как есть. И комментарий у `Listing._create_table`, утверждавший, что метод держат ради тестов, не подтвердился: тесты ссылаются на модульную функцию `create_table`, а не на метод.
|
||||||
|
|
||||||
|
У `Converter._output_path` заодно убран и параметр конструктора — оставлять параметр, который класс игнорирует, смысла нет. `pipeline.py` был единственным вызывающим, его вызов переведён на именованные аргументы, чтобы остальные позиционные не съехали.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Гигиена репозитория
|
||||||
|
|
||||||
|
Снято с отслеживания, файлы остались на диске: `ReadMe.docx`, `md2gost/README.docx`, `tmp_*.docx`, `README.md1`, `examples/example_out.docx`, `examples/toc_*.docx`, `tests/_idef0_preview*`, `latex/mirea/Settings/FiraCode-Regular.otf` (220 КБ, ни одной ссылки в `.tex`), каталог `ad/`, `md2gost.user.json`.
|
||||||
|
|
||||||
|
Перенесено с сохранением истории: `_method_extract.txt` → `docs/reference/method-mirea-2022.txt`, `tests/extended_mardown/` → `tests/extended_markdown/`.
|
||||||
|
|
||||||
|
`md2gost.user.json` содержал реальные ФИО и группу. Убран из индекса, файл на диске остался рабочим, рядом появился `md2gost.user.json.example` с пустыми полями — структура сгенерирована из `UserProfile()` и `DocumentMetadata()`, так что совпадает с тем, что пишет `save_user_profile`.
|
||||||
|
|
||||||
|
### Зависимости
|
||||||
|
|
||||||
|
Легаси-секции `[tool.poetry]`, `poetry.lock` и `requirements.txt` удалены, канон — `[project]` плюс `uv.lock`.
|
||||||
|
|
||||||
|
При этом выяснилось, что `matplotlib` был объявлен **только** в удаляемой Poetry-секции, хотя импортируется в рантайме из `find_font.py` на всех платформах кроме Linux. Добавлено `matplotlib>=3.7; sys_platform != 'linux'` в `[project].dependencies`. Также поправлен `pywin32` — он был добавлен без маркера платформы, что ломало бы `pip install .` на Linux-CI, теперь `pywin32; sys_platform == 'win32'`.
|
||||||
|
|
||||||
|
Поле `readme` исправлено с `README.md` на фактическое `ReadMe.md` — расхождение регистра сломало бы сборку колеса на Linux.
|
||||||
|
|
||||||
|
### CI
|
||||||
|
|
||||||
|
`example-generator.yml` не запускался ни разу: триггер стоял на ветку `main`, а репозиторий на `master`. Исправлено в обоих триггерах. Артефакт грузился из `example.docx`, тогда как `default_output_path` кладёт файл рядом с входным — путь исправлен на `examples/example.docx`. Имя воркфлоу совпадало с `pytest.yml`, переименовано в `Example document`. Обновлены `upload-artifact@v3` → `v4`, `checkout@v3` → `v4`, `setup-python@v3` → `v5`, добавлен `apt-get update` перед установкой шрифтов.
|
||||||
|
|
||||||
|
### Тесты
|
||||||
|
|
||||||
|
Два метода с именем `test_count_lines_courier7` затирали друг друга — второй переименован, расшадоуенный первый **проходит**, латентного бага не вскрылось. Голые `return` в двух win32-тестах `test_pipeline.py` заменены на `@pytest.mark.skipif` (раньше они проходили без единой проверки). Опечатка в имени каталога `extended_mardown` исправлена.
|
||||||
|
|
||||||
|
Кстати про нумерацию: `test_count_lines_courier7` — это индекс 7 в серии от `courier1` до `courier10`, а не размер 7 пт. Третьего размера шрифта не существует, есть только 10 в `style_config` и 12 в фикстуре и калибровке.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Дедупликация сиблинг-конвертеров
|
||||||
|
|
||||||
|
`md2latex/emitter.py` и `md2fodt/emitter.py` содержали примерно 150-200 строк почти идентичного обхода AST: диспетчер блоков, инлайн-обходчик, детектор строк библиографии, машина состояний `pending_caption`, словарь русских подписей ссылок.
|
||||||
|
|
||||||
|
Создан отдельный верхнеуровневый пакет `mdexport` — внутрь `md2gost` его класть было нельзя, а размещение в одном из экспортёров сделало бы один бэкенд зависимым от другого. Три модуля:
|
||||||
|
|
||||||
|
- `blocks.py` — `walk_blocks`, `walk_paragraph`, ABC `BlockVisitor`, кортеж отложенной подписи и `take_caption()`, детектор строк библиографии, парсинг заголовка картинки `%id Подпись`, `is_contents_heading`
|
||||||
|
- `inline.py` — `render_inline`, ABC `InlineRenderer`, таблица русских названий ссылок
|
||||||
|
- `labels.py` — `sanitize_label`, параметризованный разделителем: `:` для LaTeX, `-` для XML `NCName`
|
||||||
|
|
||||||
|
`escape_text` остался в бэкендах — там разные задачи, карта символов LaTeX против `saxutils` для XML.
|
||||||
|
|
||||||
|
Эмиттеры сократились на 119 строк, сам общий пакет занял 309, из которых около 120 — новый интерфейсный каркас: объявления абстрактных методов, докстринги, реэкспорты. По суммарному LOC выигрыш скромный, реальный смысл в том, что перечень типов узлов и порядок элементов теперь существуют в одном месте. Раньше новый тип узла приходилось добавлять в две цепочки `if/elif`, которые уже разъехались: в `md2latex` жил мёртвый словарь `cat`, которого на стороне FODT не было вовсе.
|
||||||
|
|
||||||
|
Побайтовая сверка: шесть снимков (`example.tex`, `example.fodt`, `example_emdash.fodt`, `pis_custom.tex`, `pis_custom.fodt`, `pis_custom_emdash.fodt`) совпали по SHA-256. Но образцы не дотягиваются до курсива, полужирного, кодовых вставок, ссылок, разрывов строк, цитат, вложенных списков, отсутствующих файлов картинок и экранирования специальных символов LaTeX — поэтому дооптимизационные эмиттеры были дополнительно выкачены из `HEAD` в temp и прогнаны на стресс-документе, покрывающем эти пути. `stress.tex`, `stress.fodt`, `stress_emdash.fodt` тоже идентичны.
|
||||||
|
|
||||||
|
`word2md/walker.py`: функция `_handle_paragraph` на 207 строк превращена в тело из 27 строк — собирается неизменяемая запись `_Para` с четырьмя фактами, нужными всем ветвям, затем прогоняется кортеж из одиннадцати обработчиков до первого, вернувшего не `None`. Возврат `None` означает «не мой случай, идём дальше», что сохранило два исходных сквозных пути: подпись со стилем подписи, чей текст не парсится, и строку со стилем библиографии без маркера `[n]`.
|
||||||
|
|
||||||
|
Тем же способом проверено и здесь: восемь конвертаций (синтетический DOCX с титульной частью, стилем и полем содержания, подписями продолжений, неразбираемой подписью, кодовыми абзацами, списками через `numPr` и через отступ, разрывами страниц, строкой библиографии без маркера, плюс полный круг `example.md` → DOCX → Markdown, каждая с переключением `keep_toc_pages` и `emit_page_breaks`) дают побайтово идентичный Markdown против дорефакторного обходчика.
|
||||||
|
|
||||||
|
Попутно убраны три мёртвых фрагмента: повторный вызов чистой функции при `parsed is None and kind`, no-op `if kind and parsed.kind != kind: pass` (из-за которого весь поиск `caption_kind_from_style` оказывался мёртвым, вместе с импортом), и `try/except` вокруг `re.match` по значению, которое всегда строка.
|
||||||
|
|
||||||
|
**Два следствия, требующие внимания.** `mdexport/` остался untracked, при этом уже прописан в `pyproject.toml` и импортируется обоими конвертерами — это пункт P0 в [TODO.md](TODO.md). И при разборе обнаружился реальный баг потери данных в обработчиках списков `word2md`, существовавший и до правок: он сохранён намеренно ради поведенческой нейтральности и описан в [TODO.md](TODO.md) с механизмом и порядком починки.
|
||||||
|
|
||||||
|
Спецификации PyInstaller править не пришлось — они идут по статическим импортам.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Как это делалось
|
||||||
|
|
||||||
|
Работа шла шестью параллельными исполнителями, разведёнными по владению файлами, чтобы не наступать друг другу на правки. Волна 1: фиксы багов, кэш горячего пути, диаграммы, чистка репозитория. Волна 2: двойной рендер вместе с мёртвым кодом внутри пакета, поскольку они делят `renderer.py`. Отдельным потоком шли сиблинг-конвертеры — единственная область, не пересекавшаяся ни с чем.
|
||||||
|
|
||||||
|
Три требования, заданных жёстко всем исполнителям: правки, меняющие раскладку страницы, доказываются побайтовым сравнением вывода, а не чтением кода; вопросы Фазы 1a не трогаются без решения владельца; коммиты и деструктивные git-команды запрещены.
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
# Ручная чистка git
|
||||||
|
|
||||||
|
Команды для удаления мусора из репозитория. Всё под PowerShell — помни, что `&&` там не работает как разделитель, используй `;` или отдельные вызовы.
|
||||||
|
|
||||||
|
Файл описывает то, что осталось сделать руками. То, что уже сделано агентами и лежит в staging, перечислено в конце для сверки.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Подготовка
|
||||||
|
|
||||||
|
Чтобы git печатал кириллические имена читаемо, а не как `\320\242...`:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git config core.quotepath false
|
||||||
|
```
|
||||||
|
|
||||||
|
Снимок текущего состояния — на случай, если захочется откатиться:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git status --short > ..\git-status-before.txt
|
||||||
|
git stash list
|
||||||
|
```
|
||||||
|
|
||||||
|
Ничего из перечисленного ниже не переписывает историю и не трогает remote, так что откат всегда возможен через `git restore --staged <path>`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. `Other_Code/` — 59 файлов, 1158 КБ
|
||||||
|
|
||||||
|
Что это: схемы OOXML (`*.xsd`), скрипты pack/unpack/validate, `document.py`, `utilities.py`, `docx-js.md`, `SKILL.md`. Справочный материал.
|
||||||
|
|
||||||
|
Почему можно убирать: ни одного импорта из `md2gost`, `word2md`, `md2latex`, `md2fodt`, `tests/`, ни упоминания в `.spec`, `.bat`, `.github/workflows/`, `pyproject.toml`. Единственные ссылки — раздел 12 в `PROJECT.md` и внутренние перекрёстные ссылки самого каталога.
|
||||||
|
|
||||||
|
Проверь сам, прежде чем удалять:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git grep -n "Other_Code" -- ':!Other_Code' ':!PROJECT.md'
|
||||||
|
git grep -nE "^\s*(import|from)\s+(scripts|validation)\b" -- 'md2gost' 'word2md' 'md2latex' 'md2fodt' 'tests'
|
||||||
|
```
|
||||||
|
|
||||||
|
Ожидаемый результат обеих команд — пусто.
|
||||||
|
|
||||||
|
### Вариант А: снять с отслеживания, файлы оставить на диске
|
||||||
|
|
||||||
|
Подходит, если хочешь держать схемы под рукой локально.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git rm -r --cached Other_Code
|
||||||
|
Add-Content -Path .gitignore -Value "`n# Справочные схемы OOXML (не часть продукта)`n/Other_Code/"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Вариант Б: удалить полностью
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git rm -r Other_Code
|
||||||
|
```
|
||||||
|
|
||||||
|
### Вариант В: вынести в отдельную ветку и потом удалить
|
||||||
|
|
||||||
|
Если материал ценен, но не должен висеть в основной ветке:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git subtree split --prefix=Other_Code -b reference/ooxml-schemas
|
||||||
|
git rm -r Other_Code
|
||||||
|
```
|
||||||
|
|
||||||
|
Ветка `reference/ooxml-schemas` останется локально с полной историей каталога.
|
||||||
|
|
||||||
|
После любого варианта подчисти упоминание в `PROJECT.md`, раздел 12.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Личные документы в корне
|
||||||
|
|
||||||
|
```
|
||||||
|
Практическая_работа_6.docx 42 КБ
|
||||||
|
ТарасовИА_КР_v3.docx 2326 КБ
|
||||||
|
```
|
||||||
|
|
||||||
|
Та же категория, что и `md2gost.user.json`: персональные данные, причём ФИО прямо в имени файла. Ни одной ссылки из кода, тестов или сборочных скриптов.
|
||||||
|
|
||||||
|
Снять с отслеживания, файлы оставить на диске:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git rm --cached "Практическая_работа_6.docx"
|
||||||
|
git rm --cached "ТарасовИА_КР_v3.docx"
|
||||||
|
```
|
||||||
|
|
||||||
|
Если кириллица в аргументах ломается, обойди её через pathspec по расширению и отбери нужное вручную:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git ls-files "*.docx"
|
||||||
|
```
|
||||||
|
|
||||||
|
Правило в `.gitignore`, чтобы не вернулись:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Add-Content -Path .gitignore -Value "`n# Личные документы (ФИО в имени файла)`n/*.docx`n!md2gost/Template.docx`n!md2gost/TitleTemplate.docx"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Осторожно с этим правилом.** `md2gost/Template.docx` и `md2gost/TitleTemplate.docx` — живые файлы, они force-include в колесо через `pyproject.toml` и попадают в exe через `.spec`. Правило выше их исключает, но проверь результат:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git check-ignore -v md2gost/Template.docx md2gost/TitleTemplate.docx
|
||||||
|
```
|
||||||
|
|
||||||
|
Обе строки должны вернуть пусто (то есть файлы НЕ игнорируются). Если вернули правило — убери `/*.docx` и перечисли два личных файла поимённо.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Проверка результата
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git status --short
|
||||||
|
git ls-files | Measure-Object | Select-Object -ExpandProperty Count
|
||||||
|
```
|
||||||
|
|
||||||
|
Размер отслеживаемого дерева до и после:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
(git ls-files | ForEach-Object { (Get-Item -LiteralPath $_).Length } | Measure-Object -Sum).Sum/1MB
|
||||||
|
```
|
||||||
|
|
||||||
|
Убедись, что ничего живого не выпало:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.venv\Scripts\python.exe -c "import md2gost, word2md, md2latex, md2fodt; print('ok')"
|
||||||
|
$env:PYTHONIOENCODING="utf-8"; .venv\Scripts\python.exe -m md2gost --help
|
||||||
|
.venv\Scripts\python.exe -m pytest -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Переменная `PYTHONIOENCODING` нужна из-за cp1251 в консоли — без неё `--help` падает на символе `→`, хотя код в порядке.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Про историю: снятие с отслеживания её не чистит
|
||||||
|
|
||||||
|
Это важно понимать. `git rm --cached` убирает файл из индекса и будущих коммитов, но все прошлые коммиты остаются нетронутыми. То есть:
|
||||||
|
|
||||||
|
- `md2gost.user.json` с твоими ФИО и группой **по-прежнему лежит в истории**
|
||||||
|
- `ТарасовИА_КР_v3.docx` тоже, все 2.3 МБ, в каждом коммите, где он менялся
|
||||||
|
- удалённые прототипы `mm2.py`/`mm3.py` и `Other_Code/` продолжают занимать место в `.git`
|
||||||
|
|
||||||
|
Если репозиторий приватный и таким останется — этого достаточно, дальше можно не читать.
|
||||||
|
|
||||||
|
Если планируешь публиковать, историю надо переписывать. Инструмент — `git filter-repo` (BFG устарел):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uvx git-filter-repo --path md2gost.user.json --invert-paths
|
||||||
|
uvx git-filter-repo --path "ТарасовИА_КР_v3.docx" --invert-paths
|
||||||
|
uvx git-filter-repo --path Other_Code --invert-paths
|
||||||
|
```
|
||||||
|
|
||||||
|
Перед этим обязательно:
|
||||||
|
|
||||||
|
1. Сделай полную копию каталога `.git`, а лучше всего репозитория — операция необратима.
|
||||||
|
2. Учти, что все хеши коммитов сменятся. Если репозиторий уже где-то запушен, всем придётся клонировать заново.
|
||||||
|
3. Прогони `git reflog expire --expire=now --all; git gc --prune=now --aggressive`, иначе объекты останутся в паке.
|
||||||
|
|
||||||
|
Я специально не делал это автоматически: перезапись истории — необратимая операция, её стоит запускать осознанно и на копии.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Приложение: что уже сделано агентами
|
||||||
|
|
||||||
|
Всё перечисленное лежит в staging, **не закоммичено**. Просмотри `git diff --cached` перед коммитом.
|
||||||
|
|
||||||
|
Удалено с диска (мёртвые прототипы, 6777 строк):
|
||||||
|
|
||||||
|
```
|
||||||
|
mm2.py mm3.py mm4.py mm5.py main12.py main.py
|
||||||
|
asd.py parser.py font.py _audit_tmp.py
|
||||||
|
poetry.lock requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Снято с отслеживания, файлы остались на диске:
|
||||||
|
|
||||||
|
```
|
||||||
|
ReadMe.docx md2gost/README.docx
|
||||||
|
tmp_listing_word.md/.docx tmp_readme_word.docx
|
||||||
|
tmp_readme_word2.docx README.md1
|
||||||
|
examples/example_out.docx examples/toc_manual.docx
|
||||||
|
examples/toc_native.docx tests/_idef0_preview*.png/.svg
|
||||||
|
latex/mirea/Settings/FiraCode-Regular.otf
|
||||||
|
ad/ (4 файла) md2gost.user.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Перенесено с сохранением истории:
|
||||||
|
|
||||||
|
```
|
||||||
|
_method_extract.txt -> docs/reference/method-mirea-2022.txt
|
||||||
|
tests/extended_mardown/ -> tests/extended_markdown/
|
||||||
|
```
|
||||||
|
|
||||||
|
Добавлено: `md2gost.user.json.example` с пустыми полями.
|
||||||
|
|
||||||
|
### Побочный эффект правил игнора, о котором стоит знать
|
||||||
|
|
||||||
|
Новые правила `/tmp_*.png` и `/tmp_*.puml` захватили рабочие черновики `tmp_org_full.*`, `tmp_org_uml.*`, `tmp_arch.*` — они пропали из `git status`, хотя на диске целы. Если какой-то из них нужен под версионным контролем, добавь исключение:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Add-Content -Path .gitignore -Value "!/tmp_arch.puml"
|
||||||
|
```
|
||||||
|
|
||||||
|
Каталог `ai/` не тронут и остаётся untracked.
|
||||||
+2
-2
@@ -5,9 +5,9 @@
|
|||||||
Нужен Python 3.10+.
|
Нужен Python 3.10+.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install -e .
|
uv sync # окружение по uv.lock (канонический lock-файл)
|
||||||
# или
|
# или
|
||||||
poetry install
|
pip install -e .
|
||||||
```
|
```
|
||||||
|
|
||||||
Для продолжения таблиц/листингов через Word COM (режим `word` по умолчанию) на Windows:
|
Для продолжения таблиц/листингов через Word COM (режим `word` по умолчанию) на Windows:
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,860 +0,0 @@
|
|||||||
import docx.text.font as Font
|
|
||||||
|
|
||||||
from typing import Optional, Union, Dict, Any
|
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class ThemeFont(Enum):
|
|
||||||
MAJOR_ASCII = "majorAscii"
|
|
||||||
MAJOR_H_ANSI = "majorHAnsi"
|
|
||||||
MAJOR_EAST_ASIAN = "majorEastAsia"
|
|
||||||
MAJOR_BIDI = "majorBidi"
|
|
||||||
MINOR_ASCII = "minorAscii"
|
|
||||||
MINOR_H_ANSI = "minorHAnsi"
|
|
||||||
MINOR_EAST_ASIAN = "minorEastAsia"
|
|
||||||
MINOR_BIDI = "minorBidi"
|
|
||||||
|
|
||||||
|
|
||||||
class Font:
|
|
||||||
"""
|
|
||||||
Класс, представляющий форматирование шрифта для пробега текста (run) в Word.
|
|
||||||
Поддерживает три-state значения (True/False/None) и множественные шрифты.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
# Основные шрифты
|
|
||||||
self._ascii: Optional[str] = None
|
|
||||||
self._h_ansi: Optional[str] = None
|
|
||||||
self._east_asia: Optional[str] = None
|
|
||||||
self._cs: Optional[str] = None
|
|
||||||
|
|
||||||
# Тематические шрифты (меньший приоритет)
|
|
||||||
self._ascii_theme: Optional[ThemeFont] = None
|
|
||||||
self._h_ansi_theme: Optional[ThemeFont] = None
|
|
||||||
self._east_asia_theme: Optional[ThemeFont] = None
|
|
||||||
self._cs_theme: Optional[ThemeFont] = None
|
|
||||||
|
|
||||||
# Три-state булевы свойства (None = унаследовано)
|
|
||||||
self._bold: Optional[bool] = None
|
|
||||||
self._italic: Optional[bool] = None
|
|
||||||
self._small_caps: Optional[bool] = None
|
|
||||||
self._strike: Optional[bool] = None
|
|
||||||
self._double_strike: Optional[bool] = None
|
|
||||||
self._outline: Optional[bool] = None
|
|
||||||
self._shadow: Optional[bool] = None
|
|
||||||
self._emboss: Optional[bool] = None
|
|
||||||
self._imprint: Optional[bool] = None
|
|
||||||
self._hidden: Optional[bool] = None
|
|
||||||
self._all_caps: Optional[bool] = None
|
|
||||||
self._complex_script: Optional[bool] = None
|
|
||||||
self._rtl: Optional[bool] = None
|
|
||||||
self._no_proof: Optional[bool] = None
|
|
||||||
|
|
||||||
# --- Свойство: name (основное имя шрифта) ---
|
|
||||||
@property
|
|
||||||
def name(self) -> Optional[str]:
|
|
||||||
"""Возвращает имя основного шрифта (w:ascii)."""
|
|
||||||
return self._ascii
|
|
||||||
|
|
||||||
@name.setter
|
|
||||||
def name(self, value: Optional[str]) -> None:
|
|
||||||
"""
|
|
||||||
Устанавливает w:ascii и w:hAnsi одновременно.
|
|
||||||
Если значение None — оба атрибута удаляются.
|
|
||||||
"""
|
|
||||||
if value is None:
|
|
||||||
self._ascii = None
|
|
||||||
self._h_ansi = None
|
|
||||||
else:
|
|
||||||
self._ascii = str(value)
|
|
||||||
self._h_ansi = str(value)
|
|
||||||
|
|
||||||
# --- Другие шрифты ---
|
|
||||||
@property
|
|
||||||
def east_asia(self) -> Optional[str]:
|
|
||||||
return self._east_asia
|
|
||||||
|
|
||||||
@east_asia.setter
|
|
||||||
def east_asia(self, value: Optional[str]) -> None:
|
|
||||||
self._east_asia = value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def complex_script(self) -> Optional[str]:
|
|
||||||
return self._cs
|
|
||||||
|
|
||||||
@complex_script.setter
|
|
||||||
def complex_script(self, value: Optional[str]) -> None:
|
|
||||||
self._cs = value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def ascii_theme(self) -> Optional[ThemeFont]:
|
|
||||||
return self._ascii_theme
|
|
||||||
|
|
||||||
@ascii_theme.setter
|
|
||||||
def ascii_theme(self, theme_font: Optional[ThemeFont]) -> None:
|
|
||||||
if isinstance(theme_font, ThemeFont) or theme_font is None:
|
|
||||||
self._ascii_theme = theme_font
|
|
||||||
else:
|
|
||||||
raise ValueError("ascii_theme must be a ThemeFont or None")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def h_ansi_theme(self) -> Optional[ThemeFont]:
|
|
||||||
return self._h_ansi_theme
|
|
||||||
|
|
||||||
@h_ansi_theme.setter
|
|
||||||
def h_ansi_theme(self, theme_font: Optional[ThemeFont]) -> None:
|
|
||||||
self._h_ansi_theme = theme_font
|
|
||||||
|
|
||||||
# --- Три-state Boolean Properties (Toggle properties marked) ---
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bold(self) -> Optional[bool]:
|
|
||||||
return self._bold
|
|
||||||
|
|
||||||
@bold.setter
|
|
||||||
def bold(self, value: Optional[bool]) -> None:
|
|
||||||
if value in (True, False, None):
|
|
||||||
self._bold = value
|
|
||||||
else:
|
|
||||||
raise ValueError("bold must be True, False, or None")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def italic(self) -> Optional[bool]:
|
|
||||||
return self._italic
|
|
||||||
|
|
||||||
@italic.setter
|
|
||||||
def italic(self, value: Optional[bool]) -> None:
|
|
||||||
if value in (True, False, None):
|
|
||||||
self._italic = value
|
|
||||||
else:
|
|
||||||
raise ValueError("italic must be True, False, or None")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def small_caps(self) -> Optional[bool]:
|
|
||||||
return self._small_caps
|
|
||||||
|
|
||||||
@small_caps.setter
|
|
||||||
def small_caps(self, value: Optional[bool]) -> None:
|
|
||||||
if value in (True, False, None):
|
|
||||||
self._small_caps = value
|
|
||||||
else:
|
|
||||||
raise ValueError("small_caps must be True, False, or None")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def strike(self) -> Optional[bool]:
|
|
||||||
return self._strike
|
|
||||||
|
|
||||||
@strike.setter
|
|
||||||
def strike(self, value: Optional[bool]) -> None:
|
|
||||||
if value in (True, False, None):
|
|
||||||
self._strike = value
|
|
||||||
else:
|
|
||||||
raise ValueError("strike must be True, False, or None")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def double_strike(self) -> Optional[bool]:
|
|
||||||
return self._double_strike
|
|
||||||
|
|
||||||
@double_strike.setter
|
|
||||||
def double_strike(self, value: Optional[bool]) -> None:
|
|
||||||
if value in (True, False, None):
|
|
||||||
self._double_strike = value
|
|
||||||
else:
|
|
||||||
raise ValueError("double_strike must be True, False, or None")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def outline(self) -> Optional[bool]:
|
|
||||||
return self._outline
|
|
||||||
|
|
||||||
@outline.setter
|
|
||||||
def outline(self, value: Optional[bool]) -> None:
|
|
||||||
if value in (True, False, None):
|
|
||||||
self._outline = value
|
|
||||||
else:
|
|
||||||
raise ValueError("outline must be True, False, or None")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def shadow(self) -> Optional[bool]:
|
|
||||||
return self._shadow
|
|
||||||
|
|
||||||
@shadow.setter
|
|
||||||
def shadow(self, value: Optional[bool]) -> None:
|
|
||||||
if value in (True, False, None):
|
|
||||||
self._shadow = value
|
|
||||||
else:
|
|
||||||
raise ValueError("shadow must be True, False, or None")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def emboss(self) -> Optional[bool]:
|
|
||||||
return self._emboss
|
|
||||||
|
|
||||||
@emboss.setter
|
|
||||||
def emboss(self, value: Optional[bool]) -> None:
|
|
||||||
if value in (True, False, None):
|
|
||||||
self._emboss = value
|
|
||||||
else:
|
|
||||||
raise ValueError("emboss must be True, False, or None")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def imprint(self) -> Optional[bool]:
|
|
||||||
return self._imprint
|
|
||||||
|
|
||||||
@imprint.setter
|
|
||||||
def imprint(self, value: Optional[bool]) -> None:
|
|
||||||
if value in (True, False, None):
|
|
||||||
self._imprint = value
|
|
||||||
else:
|
|
||||||
raise ValueError("imprint must be True, False, or None")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def hidden(self) -> Optional[bool]:
|
|
||||||
return self._hidden
|
|
||||||
|
|
||||||
@hidden.setter
|
|
||||||
def hidden(self, value: Optional[bool]) -> None:
|
|
||||||
if value in (True, False, None):
|
|
||||||
self._hidden = value
|
|
||||||
else:
|
|
||||||
raise ValueError("hidden must be True, False, or None")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def all_caps(self) -> Optional[bool]:
|
|
||||||
return self._all_caps
|
|
||||||
|
|
||||||
@all_caps.setter
|
|
||||||
def all_caps(self, value: Optional[bool]) -> None:
|
|
||||||
if value in (True, False, None):
|
|
||||||
self._all_caps = value
|
|
||||||
else:
|
|
||||||
raise ValueError("all_caps must be True, False, or None")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def cs(self) -> Optional[bool]:
|
|
||||||
return self._complex_script
|
|
||||||
|
|
||||||
@cs.setter
|
|
||||||
def cs(self, value: Optional[bool]) -> None:
|
|
||||||
if value in (True, False, None):
|
|
||||||
self._complex_script = value
|
|
||||||
else:
|
|
||||||
raise ValueError("cs must be True, False, or None")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def rtl(self) -> Optional[bool]:
|
|
||||||
return self._rtl
|
|
||||||
|
|
||||||
@rtl.setter
|
|
||||||
def rtl(self, value: Optional[bool]) -> None:
|
|
||||||
if value in (True, False, None):
|
|
||||||
self._rtl = value
|
|
||||||
else:
|
|
||||||
raise ValueError("rtl must be True, False, or None")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def no_proof(self) -> Optional[bool]:
|
|
||||||
return self._no_proof
|
|
||||||
|
|
||||||
@no_proof.setter
|
|
||||||
def no_proof(self, value: Optional[bool]) -> None:
|
|
||||||
if value in (True, False, None):
|
|
||||||
self._no_proof = value
|
|
||||||
else:
|
|
||||||
raise ValueError("no_proof must be True, False, or None")
|
|
||||||
|
|
||||||
# --- Методы сериализации в XML / словарь ---
|
|
||||||
def to_xml_dict(self) -> Dict[str, Any]:
|
|
||||||
"""Преобразует текущее состояние шрифта в словарь, имитирующий XML."""
|
|
||||||
r_pr = {}
|
|
||||||
|
|
||||||
fonts = {}
|
|
||||||
if self._ascii:
|
|
||||||
fonts['ascii'] = self._ascii
|
|
||||||
if self._h_ansi:
|
|
||||||
fonts['hAnsi'] = self._h_ansi
|
|
||||||
if self._east_asia:
|
|
||||||
fonts['eastAsia'] = self._east_asia
|
|
||||||
if self._cs:
|
|
||||||
fonts['cs'] = self._cs
|
|
||||||
if self._ascii_theme:
|
|
||||||
fonts['asciiTheme'] = self._ascii_theme.value
|
|
||||||
if self._h_ansi_theme:
|
|
||||||
fonts['hAnsiTheme'] = self._h_ansi_theme.value
|
|
||||||
if self._east_asia_theme:
|
|
||||||
fonts['eastAsiaTheme'] = self._east_asia_theme.value
|
|
||||||
if self._cs_theme:
|
|
||||||
fonts['csTheme'] = self._cs_theme.value
|
|
||||||
|
|
||||||
if fonts:
|
|
||||||
r_pr['rFonts'] = fonts
|
|
||||||
|
|
||||||
# Boolean properties (если не None)
|
|
||||||
bool_map = {
|
|
||||||
'b': self._bold,
|
|
||||||
'i': self._italic,
|
|
||||||
'caps': self._all_caps,
|
|
||||||
'smallCaps': self._small_caps,
|
|
||||||
'strike': self._strike,
|
|
||||||
'dstrike': self._double_strike,
|
|
||||||
'outline': self._outline,
|
|
||||||
'shadow': self._shadow,
|
|
||||||
'emboss': self._emboss,
|
|
||||||
'imprint': self._imprint,
|
|
||||||
'vanish': self._hidden,
|
|
||||||
'cs': self._complex_script,
|
|
||||||
'rtl': self._rtl,
|
|
||||||
'noProof': self._no_proof
|
|
||||||
}
|
|
||||||
|
|
||||||
for tag, val in bool_map.items():
|
|
||||||
if val is not None:
|
|
||||||
r_pr[tag] = 'on' if val else 'off'
|
|
||||||
|
|
||||||
return r_pr
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"<Font name={self.name!r} bold={self.bold} italic={self.italic}>"
|
|
||||||
|
|
||||||
|
|
||||||
class FontManager:
|
|
||||||
"""
|
|
||||||
Централизованный менеджер шрифтов, использующий темы.
|
|
||||||
Может устанавливать глобальные шрифты (major/minor) для разных языковых групп.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._theme_fonts: Dict[ThemeFont, str] = {}
|
|
||||||
|
|
||||||
def set_theme_font(self, theme_type: ThemeFont, font_name: str) -> None:
|
|
||||||
"""Устанавливает шрифт для определённой темы."""
|
|
||||||
self._theme_fonts[theme_type] = font_name
|
|
||||||
|
|
||||||
def get_theme_font(self, theme_type: ThemeFont) -> Optional[str]:
|
|
||||||
"""Получает шрифт по типу темы."""
|
|
||||||
return self._theme_fonts.get(theme_type)
|
|
||||||
|
|
||||||
def apply_theme_to_font(self, font: Font) -> None:
|
|
||||||
"""Применяет тему к объекту Font, если явно не задан основной шрифт."""
|
|
||||||
# Только если w:ascii не задан, используем w:asciiTheme
|
|
||||||
if font._ascii is None and font._ascii_theme:
|
|
||||||
font._ascii = self.get_theme_font(font._ascii_theme)
|
|
||||||
if font._h_ansi is None and font._h_ansi_theme:
|
|
||||||
font._h_ansi = self.get_theme_font(font._h_ansi_theme)
|
|
||||||
if font._east_asia is None and font._east_asia_theme:
|
|
||||||
font._east_asia = self.get_theme_font(font._east_asia_theme)
|
|
||||||
if font._cs is None and font._cs_theme:
|
|
||||||
font._cs = self.get_theme_font(font._cs_theme)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"<FontManager themes={len(self._theme_fonts)}>"
|
|
||||||
|
|
||||||
|
|
||||||
from enum import Enum
|
|
||||||
from typing import Optional, Dict, Any, List
|
|
||||||
|
|
||||||
|
|
||||||
class StyleType(Enum):
|
|
||||||
PARAGRAPH = "paragraph"
|
|
||||||
CHARACTER = "character"
|
|
||||||
TABLE = "table"
|
|
||||||
NUMBERING = "numbering"
|
|
||||||
|
|
||||||
|
|
||||||
class Style:
|
|
||||||
"""
|
|
||||||
Представление одного стиля Word (<w:style>).
|
|
||||||
Поддерживает атрибуты: имя, ID, тип, базовый стиль, приоритет, поведение и форматирование.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
style_id: str,
|
|
||||||
type_: StyleType,
|
|
||||||
name: Optional[str] = None,
|
|
||||||
default: bool = False,
|
|
||||||
custom_style: bool = False,
|
|
||||||
):
|
|
||||||
self.style_id = style_id
|
|
||||||
self.type = type_
|
|
||||||
self.default = default
|
|
||||||
self.custom_style = custom_style
|
|
||||||
|
|
||||||
# Основные свойства
|
|
||||||
self._name: Optional[str] = name or style_id # fallback на styleId
|
|
||||||
self._based_on: Optional[str] = None # ссылка на другой styleId
|
|
||||||
self._next: Optional[str] = None
|
|
||||||
self._link: Optional[str] = None
|
|
||||||
|
|
||||||
# Поведенческие флаги
|
|
||||||
self._ui_priority: Optional[int] = None
|
|
||||||
self._semi_hidden: bool = False
|
|
||||||
self._unhide_when_used: bool = False
|
|
||||||
self._q_format: bool = False
|
|
||||||
self._locked: bool = False
|
|
||||||
|
|
||||||
# Форматирование
|
|
||||||
self._paragraph_format: Optional[Dict[str, Any]] = None
|
|
||||||
self._font: Optional[Dict[str, Any]] = None
|
|
||||||
self._table_properties: Optional[Dict[str, Any]] = None
|
|
||||||
self._row_properties: Optional[Dict[str, Any]] = None
|
|
||||||
self._cell_properties: Optional[Dict[str, Any]] = None
|
|
||||||
self._table_style_properties: List[Dict[str, Any]] = []
|
|
||||||
|
|
||||||
# Служебное
|
|
||||||
self._rsid: Optional[str] = None
|
|
||||||
|
|
||||||
# --- Свойства ---
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self) -> str:
|
|
||||||
return self._name or self.style_id
|
|
||||||
|
|
||||||
@name.setter
|
|
||||||
def name(self, value: str) -> None:
|
|
||||||
self._name = value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def based_on(self) -> Optional[str]:
|
|
||||||
return self._based_on
|
|
||||||
|
|
||||||
@based_on.setter
|
|
||||||
def based_on(self, style_id: Optional[str]) -> None:
|
|
||||||
self._based_on = style_id
|
|
||||||
|
|
||||||
@property
|
|
||||||
def next_style(self) -> Optional[str]:
|
|
||||||
return self._next
|
|
||||||
|
|
||||||
@next_style.setter
|
|
||||||
def next_style(self, style_id: Optional[str]) -> None:
|
|
||||||
self._next = style_id
|
|
||||||
|
|
||||||
@property
|
|
||||||
def linked_style(self) -> Optional[str]:
|
|
||||||
return self._link
|
|
||||||
|
|
||||||
@linked_style.setter
|
|
||||||
def linked_style(self, style_id: Optional[str]) -> None:
|
|
||||||
self._link = style_id
|
|
||||||
|
|
||||||
@property
|
|
||||||
def priority(self) -> Optional[int]:
|
|
||||||
return self._ui_priority
|
|
||||||
|
|
||||||
@priority.setter
|
|
||||||
def priority(self, value: Optional[int]) -> None:
|
|
||||||
if value is not None and not isinstance(value, int):
|
|
||||||
raise ValueError("priority must be an integer or None")
|
|
||||||
self._ui_priority = value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def semi_hidden(self) -> bool:
|
|
||||||
return self._semi_hidden
|
|
||||||
|
|
||||||
@semi_hidden.setter
|
|
||||||
def semi_hidden(self, value: bool) -> None:
|
|
||||||
self._semi_hidden = bool(value)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def unhide_when_used(self) -> bool:
|
|
||||||
return self._unhide_when_used
|
|
||||||
|
|
||||||
@unhide_when_used.setter
|
|
||||||
def unhide_when_used(self, value: bool) -> None:
|
|
||||||
self._unhide_when_used = bool(value)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def quick_style(self) -> bool:
|
|
||||||
return self._q_format
|
|
||||||
|
|
||||||
@quick_style.setter
|
|
||||||
def quick_style(self, value: bool) -> None:
|
|
||||||
self._q_format = bool(value)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def locked(self) -> bool:
|
|
||||||
return self._locked
|
|
||||||
|
|
||||||
@locked.setter
|
|
||||||
def locked(self, value: bool) -> None:
|
|
||||||
self._locked = bool(value)
|
|
||||||
|
|
||||||
# --- Форматирование ---
|
|
||||||
|
|
||||||
def set_paragraph_format(self, ppr: Dict[str, Any]) -> None:
|
|
||||||
self._paragraph_format = ppr
|
|
||||||
|
|
||||||
def set_font(self, rpr: Dict[str, Any]) -> None:
|
|
||||||
self._font = rpr
|
|
||||||
|
|
||||||
def set_table_properties(self, tbl_pr: Dict[str, Any]) -> None:
|
|
||||||
self._table_properties = tbl_pr
|
|
||||||
|
|
||||||
def set_row_properties(self, tr_pr: Dict[str, Any]) -> None:
|
|
||||||
self._row_properties = tr_pr
|
|
||||||
|
|
||||||
def set_cell_properties(self, tc_pr: Dict[str, Any]) -> None:
|
|
||||||
self._cell_properties = tc_pr
|
|
||||||
|
|
||||||
def add_table_style_property(self, prop: Dict[str, Any]) -> None:
|
|
||||||
self._table_style_properties.append(prop)
|
|
||||||
|
|
||||||
# --- Вспомогательные методы ---
|
|
||||||
|
|
||||||
def is_builtin(self) -> bool:
|
|
||||||
"""Возвращает True, если стиль встроенный (не пользовательский)."""
|
|
||||||
return not self.custom_style
|
|
||||||
|
|
||||||
def is_visible_in_gallery(self) -> bool:
|
|
||||||
"""Появляется ли стиль в галерее стилей?"""
|
|
||||||
return not self.semi_hidden and self.quick_style
|
|
||||||
|
|
||||||
def to_xml_dict(self) -> Dict[str, Any]:
|
|
||||||
"""Сериализация в словарь, близкий к XML."""
|
|
||||||
result = {
|
|
||||||
"w:type": self.type.value,
|
|
||||||
"w:styleId": self.style_id,
|
|
||||||
}
|
|
||||||
if self.default:
|
|
||||||
result["w:default"] = "1"
|
|
||||||
if self.custom_style:
|
|
||||||
result["w:customStyle"] = "1"
|
|
||||||
|
|
||||||
children = []
|
|
||||||
|
|
||||||
if self._name:
|
|
||||||
children.append({"w:name": {"w:val": self._name}})
|
|
||||||
if self._based_on:
|
|
||||||
children.append({"w:basedOn": {"w:val": self._based_on}})
|
|
||||||
if self._next:
|
|
||||||
children.append({"w:next": {"w:val": self._next}})
|
|
||||||
if self._link:
|
|
||||||
children.append({"w:link": {"w:val": self._link}})
|
|
||||||
if self._ui_priority is not None:
|
|
||||||
children.append({"w:uiPriority": {"w:val": str(self._ui_priority)}})
|
|
||||||
if self._semi_hidden:
|
|
||||||
children.append({"w:semiHidden": {}})
|
|
||||||
if self._unhide_when_used:
|
|
||||||
children.append({"w:unhideWhenUsed": {}})
|
|
||||||
if self._q_format:
|
|
||||||
children.append({"w:qFormat": {}})
|
|
||||||
if self._locked:
|
|
||||||
children.append({"w:locked": {}})
|
|
||||||
if self._rsid:
|
|
||||||
children.append({"w:rsid": {"w:val": self._rsid}})
|
|
||||||
|
|
||||||
if self._paragraph_format:
|
|
||||||
children.append({"w:pPr": self._paragraph_format})
|
|
||||||
if self._font:
|
|
||||||
children.append({"w:rPr": self._font})
|
|
||||||
if self._table_properties:
|
|
||||||
children.append({"w:tblPr": self._table_properties})
|
|
||||||
if self._row_properties:
|
|
||||||
children.append({"w:trPr": self._row_properties})
|
|
||||||
if self._cell_properties:
|
|
||||||
children.append({"w:tcPr": self._cell_properties})
|
|
||||||
for tsp in self._table_style_properties:
|
|
||||||
children.append({"w:tblStylePr": tsp})
|
|
||||||
|
|
||||||
if children:
|
|
||||||
result["_children"] = children
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"<Style id='{self.style_id}' type={self.type.name} name='{self.name}'>"
|
|
||||||
|
|
||||||
from typing import Iterator, Optional, Dict, List
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
class Styles:
|
|
||||||
"""
|
|
||||||
Коллекция всех стилей документа: определённых, встроенных и latent.
|
|
||||||
Имитирует поведение Word по сортировке, отображению и разрешению стилей.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._styles: Dict[str, Style] = {} # styleId -> Style
|
|
||||||
self._latent_styles: Dict[str, Dict[str, Any]] = {}
|
|
||||||
self._doc_defaults = {
|
|
||||||
"rPr": {},
|
|
||||||
"pPr": {}
|
|
||||||
}
|
|
||||||
self._latent_count = 0
|
|
||||||
self._def_semi_hidden = True
|
|
||||||
self._def_unhide_when_used = True
|
|
||||||
self._def_q_format = False
|
|
||||||
self._def_locked_state = False
|
|
||||||
self._def_ui_priority = 99
|
|
||||||
|
|
||||||
# --- Управление latent styles ---
|
|
||||||
|
|
||||||
def define_latent_style(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
ui_priority: Optional[int] = None,
|
|
||||||
semi_hidden: Optional[bool] = None,
|
|
||||||
unhide_when_used: Optional[bool] = None,
|
|
||||||
q_format: Optional[bool] = None,
|
|
||||||
locked: Optional[bool] = None,
|
|
||||||
) -> None:
|
|
||||||
"""Добавляет исключение для latent style."""
|
|
||||||
self._latent_styles[name] = {
|
|
||||||
"uiPriority": ui_priority,
|
|
||||||
"semiHidden": semi_hidden,
|
|
||||||
"unhideWhenUsed": unhide_when_used,
|
|
||||||
"qFormat": q_format,
|
|
||||||
"locked": locked,
|
|
||||||
}
|
|
||||||
|
|
||||||
def set_latent_defaults(
|
|
||||||
self,
|
|
||||||
count: int = 276,
|
|
||||||
def_semi_hidden: bool = True,
|
|
||||||
def_unhide_when_used: bool = True,
|
|
||||||
def_q_format: bool = False,
|
|
||||||
def_locked_state: bool = False,
|
|
||||||
def_ui_priority: int = 99,
|
|
||||||
) -> None:
|
|
||||||
"""Устанавливает параметры по умолчанию для latent styles."""
|
|
||||||
self._latent_count = count
|
|
||||||
self._def_semi_hidden = def_semi_hidden
|
|
||||||
self._def_unhide_when_used = def_unhide_when_used
|
|
||||||
self._def_q_format = def_q_format
|
|
||||||
self._def_locked_state = def_locked_state
|
|
||||||
self._def_ui_priority = def_ui_priority
|
|
||||||
|
|
||||||
def get_latent_behavior(self, name: str) -> Dict[str, Any]:
|
|
||||||
"""Возвращает поведенческие атрибуты для встроенного стиля."""
|
|
||||||
base = {
|
|
||||||
"semiHidden": self._def_semi_hidden,
|
|
||||||
"unhideWhenUsed": self._def_unhide_when_used,
|
|
||||||
"qFormat": self._def_q_format,
|
|
||||||
"locked": self._def_locked_state,
|
|
||||||
"uiPriority": self._def_ui_priority,
|
|
||||||
}
|
|
||||||
override = self._latent_styles.get(name, {})
|
|
||||||
base.update({k: v for k, v in override.items() if v is not None})
|
|
||||||
return base
|
|
||||||
|
|
||||||
# --- Добавление/получение стилей ---
|
|
||||||
|
|
||||||
def add_style(
|
|
||||||
self,
|
|
||||||
style_id: str,
|
|
||||||
type_: StyleType,
|
|
||||||
name: Optional[str] = None,
|
|
||||||
default: bool = False,
|
|
||||||
custom_style: bool = False,
|
|
||||||
) -> Style:
|
|
||||||
"""Создаёт и добавляет новый стиль."""
|
|
||||||
style = Style(style_id, type_, name=name, default=default, custom_style=custom_style)
|
|
||||||
self._styles[style_id] = style
|
|
||||||
return style
|
|
||||||
|
|
||||||
def get_by_id(self, style_id: str) -> Optional[Style]:
|
|
||||||
"""Получить стиль по ID."""
|
|
||||||
return self._styles.get(style_id)
|
|
||||||
|
|
||||||
def get_by_name(self, name: str) -> Optional[Style]:
|
|
||||||
"""Найти стиль по имени (регистронезависимо)."""
|
|
||||||
for style in self._styles.values():
|
|
||||||
if style.name.lower() == name.lower():
|
|
||||||
return style
|
|
||||||
return None
|
|
||||||
|
|
||||||
def __getitem__(self, key: str) -> Style:
|
|
||||||
"""Доступ как по ID, так и по имени."""
|
|
||||||
style = self.get_by_id(key)
|
|
||||||
if style is None:
|
|
||||||
style = self.get_by_name(key)
|
|
||||||
if style is None:
|
|
||||||
raise KeyError(f"Style '{key}' not found.")
|
|
||||||
return style
|
|
||||||
|
|
||||||
def __setitem__(self, key: str, style: Style) -> None:
|
|
||||||
if not isinstance(style, Style):
|
|
||||||
raise TypeError("Only Style objects can be added.")
|
|
||||||
self._styles[style.style_id] = style
|
|
||||||
|
|
||||||
def __delitem__(self, key: str) -> None:
|
|
||||||
style = self.get_by_id(key) or self.get_by_name(key)
|
|
||||||
if style is None:
|
|
||||||
raise KeyError(f"Style '{key}' not found.")
|
|
||||||
del self._styles[style.style_id]
|
|
||||||
|
|
||||||
def __contains__(self, key: str) -> bool:
|
|
||||||
return (key in self._styles) or (self.get_by_name(key) is not None)
|
|
||||||
|
|
||||||
def __len__(self) -> int:
|
|
||||||
return len(self._styles)
|
|
||||||
|
|
||||||
def __iter__(self) -> Iterator[Style]:
|
|
||||||
"""Итерация по всем определённым стилям."""
|
|
||||||
return iter(self._styles.values())
|
|
||||||
|
|
||||||
# --- Document defaults ---
|
|
||||||
|
|
||||||
def set_default_run_properties(self, rpr: Dict[str, Any]) -> None:
|
|
||||||
self._doc_defaults["rPr"] = rpr
|
|
||||||
|
|
||||||
def set_default_paragraph_properties(self, ppr: Dict[str, Any]) -> None:
|
|
||||||
self._doc_defaults["pPr"] = ppr
|
|
||||||
|
|
||||||
def get_default_font(self) -> Dict[str, Any]:
|
|
||||||
return self._doc_defaults["rPr"]
|
|
||||||
|
|
||||||
def get_default_paragraph_format(self) -> Dict[str, Any]:
|
|
||||||
return self._doc_defaults["pPr"]
|
|
||||||
|
|
||||||
# --- Списки стилей (как в интерфейсе Word) ---
|
|
||||||
|
|
||||||
def recommended_list(self) -> List[Style]:
|
|
||||||
"""Стили, отображаемые в категории 'Recommended'."""
|
|
||||||
return [
|
|
||||||
s for s in self._styles.values()
|
|
||||||
if not s.semi_hidden
|
|
||||||
]
|
|
||||||
|
|
||||||
def styles_in_use(self, used_style_ids: List[str]) -> List[Style]:
|
|
||||||
"""Стили, применённые в документе и не semiHidden."""
|
|
||||||
return [
|
|
||||||
s for s in self._styles.values()
|
|
||||||
if s.style_id in used_style_ids and not s.semi_hidden
|
|
||||||
]
|
|
||||||
|
|
||||||
def in_current_document(self) -> List[Style]:
|
|
||||||
"""Все определённые стили, кроме semiHidden."""
|
|
||||||
return [s for s in self._styles.values() if not s.semi_hidden]
|
|
||||||
|
|
||||||
def all_styles(self) -> List[Style]:
|
|
||||||
"""Все определённые стили (включая semiHidden)."""
|
|
||||||
return list(self._styles.values())
|
|
||||||
|
|
||||||
# --- Сортировка ---
|
|
||||||
|
|
||||||
def sorted_by_appearance(self) -> List[Style]:
|
|
||||||
"""
|
|
||||||
Сортировка стилей так, как они отображаются в панели стилей Word:
|
|
||||||
- По uiPriority (возрастание)
|
|
||||||
- При равном приоритете — по имени (алфавитно)
|
|
||||||
- Custom-стили без uiPriority попадают в начало (uiPriority=0)
|
|
||||||
"""
|
|
||||||
def sort_key(style: Style) -> tuple:
|
|
||||||
prio = style.priority if style.priority is not None else 0
|
|
||||||
return (prio, style.name.lower())
|
|
||||||
|
|
||||||
return sorted(self._styles.values(), key=sort_key)
|
|
||||||
|
|
||||||
# --- Генерация XML ---
|
|
||||||
|
|
||||||
def to_xml_dict(self) -> Dict[str, Any]:
|
|
||||||
result = {
|
|
||||||
"w:styles": {
|
|
||||||
"_children": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# docDefaults
|
|
||||||
if any(self._doc_defaults["rPr"]) or any(self._doc_defaults["pPr"]):
|
|
||||||
defaults = {"_children": []}
|
|
||||||
if self._doc_defaults["rPr"]:
|
|
||||||
defaults["_children"].append({
|
|
||||||
"w:rPrDefault": {
|
|
||||||
"_children": [{"w:rPr": self._doc_defaults["rPr"]}]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if self._doc_defaults["pPr"]:
|
|
||||||
defaults["_children"].append({
|
|
||||||
"w:pPrDefault": {
|
|
||||||
"_children": [{"w:pPr": self._doc_defaults["pPr"]}]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
result["w:styles"]["_children"].append(defaults)
|
|
||||||
|
|
||||||
# latentStyles
|
|
||||||
latent_children = []
|
|
||||||
for name, props in self._latent_styles.items():
|
|
||||||
exc = {"w:name": name}
|
|
||||||
if props["uiPriority"] is not None:
|
|
||||||
exc["w:uiPriority"] = str(props["uiPriority"])
|
|
||||||
if props["semiHidden"] is not None:
|
|
||||||
exc["w:semiHidden"] = "1" if props["semiHidden"] else "0"
|
|
||||||
if props["unhideWhenUsed"] is not None:
|
|
||||||
exc["w:unhideWhenUsed"] = "1" if props["unhideWhenUsed"] else "0"
|
|
||||||
if props["qFormat"] is not None:
|
|
||||||
exc["w:qFormat"] = "1" if props["qFormat"] else "0"
|
|
||||||
if props["locked"] is not None:
|
|
||||||
exc["w:locked"] = "1" if props["locked"] else "0"
|
|
||||||
latent_children.append({"w:lsdException": exc})
|
|
||||||
|
|
||||||
latent_attrs = {
|
|
||||||
"w:count": str(self._latent_count),
|
|
||||||
"w:defSemiHidden": "1" if self._def_semi_hidden else "0",
|
|
||||||
"w:defUnhideWhenUsed": "1" if self._def_unhide_when_used else "0",
|
|
||||||
"w:defQFormat": "1" if self._def_q_format else "0",
|
|
||||||
"w:defLockedState": "1" if self._def_locked_state else "0",
|
|
||||||
"w:defUIPriority": str(self._def_ui_priority),
|
|
||||||
}
|
|
||||||
result["w:styles"]["_children"].append({
|
|
||||||
"w:latentStyles": {
|
|
||||||
"_children": latent_children,
|
|
||||||
**latent_attrs
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
# Все стили
|
|
||||||
for style in self._styles.values():
|
|
||||||
result["w:styles"]["_children"].append({"w:style": style.to_xml_dict()})
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"<Styles count={len(self)} latent={len(self._latent_styles)}>"
|
|
||||||
|
|
||||||
# Создаём коллекцию стилей
|
|
||||||
styles = Styles()
|
|
||||||
|
|
||||||
# Настройка latent-стилей (встроенные по умолчанию)
|
|
||||||
styles.set_latent_defaults(count=276, def_semi_hidden=True, def_unhide_when_used=True)
|
|
||||||
styles.define_latent_style("Normal", ui_priority=0, semi_hidden=False, unhide_when_used=False, q_format=True)
|
|
||||||
styles.define_latent_style("Heading 1", ui_priority=9, q_format=True)
|
|
||||||
|
|
||||||
# Добавляем стили
|
|
||||||
normal = styles.add_style("Normal", StyleType.PARAGRAPH, name="Normal", default=True)
|
|
||||||
normal.quick_style = True
|
|
||||||
normal.semi_hidden = False
|
|
||||||
|
|
||||||
heading1 = styles.add_style("Heading1", StyleType.PARAGRAPH, name="Heading 1")
|
|
||||||
heading1.based_on = "Normal"
|
|
||||||
heading1.priority = 9
|
|
||||||
heading1.quick_style = True
|
|
||||||
heading1.semi_hidden = False
|
|
||||||
heading1.set_font({"b": {}, "sz": {"w:val": "28"}})
|
|
||||||
|
|
||||||
foobar = styles.add_style("Foobar", StyleType.PARAGRAPH, name="Foobar", custom_style=True)
|
|
||||||
foobar.priority = 50
|
|
||||||
foobar.semi_hidden = False
|
|
||||||
foobar.quick_style = True
|
|
||||||
|
|
||||||
# Установка умолчаний документа
|
|
||||||
styles.set_default_run_properties({
|
|
||||||
"rFonts": {"w:asciiTheme": "minorHAnsi"},
|
|
||||||
"sz": {"w:val": "24"}
|
|
||||||
})
|
|
||||||
styles.set_default_paragraph_properties({"spacing": {"w:after": "120"}})
|
|
||||||
|
|
||||||
# Получение стилей
|
|
||||||
print(styles["Normal"]) # <Style id='Normal' ...>
|
|
||||||
print(styles["Heading 1"]) # найдёт по имени
|
|
||||||
|
|
||||||
# Список рекомендованных
|
|
||||||
for s in styles.recommended_list():
|
|
||||||
print(s.name)
|
|
||||||
|
|
||||||
# Сортировка
|
|
||||||
for s in styles.sorted_by_appearance():
|
|
||||||
print(f"{s.name} (priority={s.priority})")
|
|
||||||
|
|
||||||
# Экспорт
|
|
||||||
xml_data = styles.to_xml_dict()
|
|
||||||
Binary file not shown.
@@ -1,32 +0,0 @@
|
|||||||
# открытие файла
|
|
||||||
# установка нужных шрифтов в документе 1.
|
|
||||||
# работа с текстом
|
|
||||||
# работа с таблицами
|
|
||||||
# -поддержка переноса таблицы
|
|
||||||
# сохранение
|
|
||||||
# Настройка оглавлений
|
|
||||||
|
|
||||||
|
|
||||||
from docx import Document
|
|
||||||
|
|
||||||
# Создаем новый документ
|
|
||||||
document = Document()
|
|
||||||
|
|
||||||
# Добавляем заголовок
|
|
||||||
document.add_heading('Пример документа Word', level=1)
|
|
||||||
|
|
||||||
# Добавляем параграф с текстом
|
|
||||||
document.add_paragraph('Это первый параграф, который был добавлен с помощью Python.')
|
|
||||||
|
|
||||||
# Добавляем еще один параграф
|
|
||||||
document.add_paragraph('Это второй параграф. Можно добавлять новый текст, как вам нужно.')
|
|
||||||
|
|
||||||
# Добавляем текст в уже существующий параграф
|
|
||||||
paragraph = document.add_paragraph('А это текст, добавленный к ')
|
|
||||||
paragraph.add_run('существующему ').italic = True
|
|
||||||
paragraph.add_run('параграфу.')
|
|
||||||
|
|
||||||
# Сохраняем документ
|
|
||||||
document.save('мой_документ.docx')
|
|
||||||
|
|
||||||
print("Документ 'мой_документ.docx' успешно создан и текст добавлен.")
|
|
||||||
@@ -1,915 +0,0 @@
|
|||||||
from abc import ABC, abstractmethod
|
|
||||||
from typing import Dict, List, Any, Optional, Union, Callable
|
|
||||||
from docx import Document
|
|
||||||
from docx.shared import Pt, Cm, RGBColor
|
|
||||||
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING
|
|
||||||
from docx.enum.style import WD_STYLE_TYPE
|
|
||||||
from docx.oxml.ns import qn
|
|
||||||
from docx.oxml import OxmlElement
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
class GostDocumentConfig:
|
|
||||||
"""Конфигурация параметров документа по ГОСТ"""
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
# Поля документа (в сантиметрах)
|
|
||||||
self.margins = kwargs.get('margins', {
|
|
||||||
'top': 2.0,
|
|
||||||
'bottom': 2.0,
|
|
||||||
'left': 3.0,
|
|
||||||
'right': 1.5
|
|
||||||
})
|
|
||||||
|
|
||||||
# Шрифты
|
|
||||||
self.font = kwargs.get('font', {
|
|
||||||
'name': 'Times New Roman',
|
|
||||||
'size': 14,
|
|
||||||
'main_language': 'ru-RU'
|
|
||||||
})
|
|
||||||
|
|
||||||
# Межстрочный интервал
|
|
||||||
self.line_spacing = kwargs.get('line_spacing', 1.5)
|
|
||||||
|
|
||||||
# Отступы для заголовков
|
|
||||||
self.heading_spacing = kwargs.get('heading_spacing', {
|
|
||||||
1: {'before': 24, 'after': 12},
|
|
||||||
2: {'before': 24, 'after': 6},
|
|
||||||
3: {'before': 18, 'after': 6}
|
|
||||||
})
|
|
||||||
|
|
||||||
# Стили заголовков
|
|
||||||
self.heading_styles = kwargs.get('heading_styles', {
|
|
||||||
1: {'size': 16, 'bold': True, 'all_caps': True, 'align': 'center'},
|
|
||||||
2: {'size': 14, 'bold': True, 'all_caps': False, 'align': 'left'},
|
|
||||||
3: {'size': 14, 'bold': True, 'italic': True, 'all_caps': False, 'align': 'left'}
|
|
||||||
})
|
|
||||||
|
|
||||||
# Стиль абзаца
|
|
||||||
self.paragraph = kwargs.get('paragraph', {
|
|
||||||
'first_line_indent': 1.25, # в см
|
|
||||||
'alignment': 'justify'
|
|
||||||
})
|
|
||||||
|
|
||||||
# Стиль таблиц
|
|
||||||
self.table = kwargs.get('table', {
|
|
||||||
'font_size': 12,
|
|
||||||
'header_bold': True
|
|
||||||
})
|
|
||||||
|
|
||||||
# Стиль подписей
|
|
||||||
self.caption = kwargs.get('caption', {
|
|
||||||
'font_size': 12,
|
|
||||||
'italic': True,
|
|
||||||
'align': 'center'
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
class GostDocumentCore(ABC):
|
|
||||||
"""Базовый класс для создания документов по ГОСТ с возможностью расширения"""
|
|
||||||
|
|
||||||
def __init__(self, config: Optional[GostDocumentConfig] = None,
|
|
||||||
title: str = "", author: str = ""):
|
|
||||||
"""
|
|
||||||
Инициализация документа с настройками ГОСТ
|
|
||||||
|
|
||||||
Args:
|
|
||||||
config: Конфигурация документа (если None - используются стандартные настройки ГОСТ)
|
|
||||||
title: Заголовок документа
|
|
||||||
author: Автор документа
|
|
||||||
"""
|
|
||||||
self.config = config or GostDocumentConfig()
|
|
||||||
self.title = title
|
|
||||||
self.author = author
|
|
||||||
self.document = Document()
|
|
||||||
self._element_processors = {}
|
|
||||||
self._setup_document()
|
|
||||||
self._create_gost_styles()
|
|
||||||
|
|
||||||
# Регистрируем базовые процессоры элементов
|
|
||||||
self.register_processor('heading', self._process_heading)
|
|
||||||
self.register_processor('paragraph', self._process_paragraph)
|
|
||||||
self.register_processor('table', self._process_table)
|
|
||||||
self.register_processor('list', self._process_list)
|
|
||||||
self.register_processor('image', self._process_image)
|
|
||||||
|
|
||||||
def _setup_document(self):
|
|
||||||
"""Настройка базовых параметров документа согласно ГОСТ"""
|
|
||||||
# Настройка полей страницы
|
|
||||||
sections = self.document.sections
|
|
||||||
for section in sections:
|
|
||||||
section.top_margin = Cm(self.config.margins['top'])
|
|
||||||
section.bottom_margin = Cm(self.config.margins['bottom'])
|
|
||||||
section.left_margin = Cm(self.config.margins['left'])
|
|
||||||
section.right_margin = Cm(self.config.margins['right'])
|
|
||||||
|
|
||||||
# Установка стандартного стиля для всего документа
|
|
||||||
style = self.document.styles['Normal']
|
|
||||||
font = style.font
|
|
||||||
font.name = self.config.font['name']
|
|
||||||
font.size = Pt(self.config.font['size'])
|
|
||||||
|
|
||||||
# Установка кириллического шрифта
|
|
||||||
font.element.rPr.rFonts.set(qn('w:eastAsia'), self.config.font['name'])
|
|
||||||
|
|
||||||
# Настройка формата абзаца
|
|
||||||
paragraph_format = style.paragraph_format
|
|
||||||
paragraph_format.line_spacing_rule = (
|
|
||||||
WD_LINE_SPACING.ONE_POINT_FIVE if self.config.line_spacing == 1.5
|
|
||||||
else WD_LINE_SPACING.DOUBLE if self.config.line_spacing == 2.0
|
|
||||||
else WD_LINE_SPACING.EXACTLY
|
|
||||||
)
|
|
||||||
paragraph_format.alignment = (
|
|
||||||
WD_ALIGN_PARAGRAPH.JUSTIFY if self.config.paragraph['alignment'] == 'justify'
|
|
||||||
else WD_ALIGN_PARAGRAPH.LEFT if self.config.paragraph['alignment'] == 'left'
|
|
||||||
else WD_ALIGN_PARAGRAPH.CENTER
|
|
||||||
)
|
|
||||||
paragraph_format.first_line_indent = Cm(self.config.paragraph['first_line_indent'])
|
|
||||||
|
|
||||||
def _create_gost_styles(self):
|
|
||||||
"""Создание стилей заголовков и других элементов в соответствии с ГОСТ"""
|
|
||||||
# Заголовок 1 уровня
|
|
||||||
self._create_heading_style(1)
|
|
||||||
# Заголовок 2 уровня
|
|
||||||
self._create_heading_style(2)
|
|
||||||
# Заголовок 3 уровня
|
|
||||||
self._create_heading_style(3)
|
|
||||||
|
|
||||||
# Стиль для списка
|
|
||||||
if 'List GOST' not in self.document.styles:
|
|
||||||
list_style = self.document.styles.add_style('List GOST', WD_STYLE_TYPE.PARAGRAPH)
|
|
||||||
list_style.base_style = self.document.styles['List']
|
|
||||||
list_style.font.name = self.config.font['name']
|
|
||||||
list_style.font.size = Pt(self.config.font['size'])
|
|
||||||
list_style.paragraph_format.left_indent = Cm(1.25)
|
|
||||||
list_style.paragraph_format.space_before = Pt(0)
|
|
||||||
list_style.paragraph_format.space_after = Pt(6)
|
|
||||||
list_style.paragraph_format.line_spacing_rule = (
|
|
||||||
WD_LINE_SPACING.ONE_POINT_FIVE if self.config.line_spacing == 1.5
|
|
||||||
else WD_LINE_SPACING.DOUBLE
|
|
||||||
)
|
|
||||||
|
|
||||||
# Стиль для подписей таблиц и рисунков
|
|
||||||
if 'Caption GOST' not in self.document.styles:
|
|
||||||
caption_style = self.document.styles.add_style('Caption GOST', WD_STYLE_TYPE.PARAGRAPH)
|
|
||||||
caption_style.base_style = self.document.styles['Caption']
|
|
||||||
caption_style.font.name = self.config.font['name']
|
|
||||||
caption_style.font.size = Pt(self.config.caption['font_size'])
|
|
||||||
caption_style.font.italic = self.config.caption['italic']
|
|
||||||
caption_style.paragraph_format.alignment = (
|
|
||||||
WD_ALIGN_PARAGRAPH.CENTER if self.config.caption['align'] == 'center'
|
|
||||||
else WD_ALIGN_PARAGRAPH.LEFT
|
|
||||||
)
|
|
||||||
caption_style.paragraph_format.space_before = Pt(6)
|
|
||||||
caption_style.paragraph_format.space_after = Pt(12)
|
|
||||||
|
|
||||||
def _create_heading_style(self, level: int):
|
|
||||||
"""Создает стиль заголовка заданного уровня"""
|
|
||||||
style_name = f'Heading {level} GOST'
|
|
||||||
|
|
||||||
if style_name in self.document.styles:
|
|
||||||
return
|
|
||||||
|
|
||||||
style = self.document.styles.add_style(style_name, WD_STYLE_TYPE.PARAGRAPH)
|
|
||||||
|
|
||||||
# Настройка шрифта
|
|
||||||
heading_config = self.config.heading_styles[level]
|
|
||||||
style.font.name = self.config.font['name']
|
|
||||||
style.font.size = Pt(heading_config['size'])
|
|
||||||
style.font.bold = heading_config['bold']
|
|
||||||
style.font.italic = heading_config.get('italic', False)
|
|
||||||
style.font.all_caps = heading_config['all_caps']
|
|
||||||
|
|
||||||
# Настройка абзаца
|
|
||||||
spacing = self.config.heading_spacing[level]
|
|
||||||
style.paragraph_format.space_before = Pt(spacing['before'])
|
|
||||||
style.paragraph_format.space_after = Pt(spacing['after'])
|
|
||||||
style.paragraph_format.line_spacing_rule = (
|
|
||||||
WD_LINE_SPACING.ONE_POINT_FIVE if self.config.line_spacing == 1.5
|
|
||||||
else WD_LINE_SPACING.DOUBLE
|
|
||||||
)
|
|
||||||
|
|
||||||
# Выравнивание
|
|
||||||
if heading_config['align'] == 'center':
|
|
||||||
style.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
||||||
elif heading_config['align'] == 'left':
|
|
||||||
style.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
|
||||||
style.paragraph_format.first_line_indent = Cm(0) # Без красной строки для заголовков
|
|
||||||
|
|
||||||
def register_processor(self, element_type: str, processor: Callable):
|
|
||||||
"""
|
|
||||||
Регистрирует обработчик для определенного типа элемента
|
|
||||||
|
|
||||||
Args:
|
|
||||||
element_type: Тип элемента (например, 'heading', 'table')
|
|
||||||
processor: Функция-обработчик
|
|
||||||
"""
|
|
||||||
self._element_processors[element_type] = processor
|
|
||||||
|
|
||||||
def get_processor(self, element_type: str) -> Optional[Callable]:
|
|
||||||
"""
|
|
||||||
Возвращает обработчик для указанного типа элемента
|
|
||||||
|
|
||||||
Args:
|
|
||||||
element_type: Тип элемента
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Обработчик или None, если не найден
|
|
||||||
"""
|
|
||||||
return self._element_processors.get(element_type)
|
|
||||||
|
|
||||||
def _process_heading(self, element: Dict[str, Any]) -> None:
|
|
||||||
"""Обрабатывает элемент заголовка"""
|
|
||||||
level = element.get('level', 1)
|
|
||||||
text = element.get('text', '')
|
|
||||||
|
|
||||||
heading = self.document.add_heading(level=0)
|
|
||||||
style_name = f'Heading {level} GOST'
|
|
||||||
|
|
||||||
if style_name in self.document.styles:
|
|
||||||
heading.style = style_name
|
|
||||||
else:
|
|
||||||
# Базовая настройка, если стиль не создан
|
|
||||||
heading.alignment = WD_ALIGN_PARAGRAPH.CENTER if level == 1 else WD_ALIGN_PARAGRAPH.LEFT
|
|
||||||
run = heading.add_run(text)
|
|
||||||
run.font.size = Pt(16 if level == 1 else 14)
|
|
||||||
run.font.bold = True
|
|
||||||
|
|
||||||
# Добавляем текст
|
|
||||||
heading.add_run(text)
|
|
||||||
|
|
||||||
def _process_paragraph(self, element: Dict[str, Any]) -> None:
|
|
||||||
"""Обрабатывает элемент абзаца"""
|
|
||||||
text = element.get('text', '')
|
|
||||||
style = element.get('style')
|
|
||||||
|
|
||||||
p = self.document.add_paragraph()
|
|
||||||
|
|
||||||
if style and style in self.document.styles:
|
|
||||||
p.style = style
|
|
||||||
|
|
||||||
# Основной текст по ГОСТ
|
|
||||||
p.paragraph_format.line_spacing_rule = (
|
|
||||||
WD_LINE_SPACING.ONE_POINT_FIVE if self.config.line_spacing == 1.5
|
|
||||||
else WD_LINE_SPACING.DOUBLE
|
|
||||||
)
|
|
||||||
p.paragraph_format.alignment = (
|
|
||||||
WD_ALIGN_PARAGRAPH.JUSTIFY if self.config.paragraph['alignment'] == 'justify'
|
|
||||||
else WD_ALIGN_PARAGRAPH.LEFT
|
|
||||||
)
|
|
||||||
p.paragraph_format.first_line_indent = Cm(self.config.paragraph['first_line_indent'])
|
|
||||||
print(text)
|
|
||||||
# Добавляем текст
|
|
||||||
p.add_run(text)
|
|
||||||
|
|
||||||
def _process_table(self, element: Dict[str, Any]) -> None:
|
|
||||||
"""Обрабатывает элемент таблицы"""
|
|
||||||
data = element.get('data', [])
|
|
||||||
headers = element.get('headers')
|
|
||||||
|
|
||||||
# Добавляем подпись таблицы
|
|
||||||
caption = self.document.add_paragraph()
|
|
||||||
caption.style = 'Caption GOST'
|
|
||||||
caption.add_run("Таблица ").bold = True
|
|
||||||
|
|
||||||
# Создаем таблицу
|
|
||||||
rows = len(data) + (1 if headers else 0)
|
|
||||||
cols = len(data[0]) if data else 0
|
|
||||||
if cols == 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
table = self.document.add_table(rows=rows, cols=cols)
|
|
||||||
table.style = 'Table Grid'
|
|
||||||
|
|
||||||
# Заполняем заголовки, если они есть
|
|
||||||
if headers:
|
|
||||||
hdr_cells = table.rows[0].cells
|
|
||||||
for i, header in enumerate(headers):
|
|
||||||
hdr_cells[i].text = header
|
|
||||||
for paragraph in hdr_cells[i].paragraphs:
|
|
||||||
for run in paragraph.runs:
|
|
||||||
run.font.bold = self.config.table['header_bold']
|
|
||||||
|
|
||||||
# Заполняем данные
|
|
||||||
start_row = 1 if headers else 0
|
|
||||||
for i, row_data in enumerate(data):
|
|
||||||
row_cells = table.rows[i + start_row].cells
|
|
||||||
for j, cell_data in enumerate(row_data):
|
|
||||||
row_cells[j].text = str(cell_data)
|
|
||||||
|
|
||||||
# Настройка шрифта для всех ячеек
|
|
||||||
for row in table.rows:
|
|
||||||
for cell in row.cells:
|
|
||||||
for paragraph in cell.paragraphs:
|
|
||||||
for run in paragraph.runs:
|
|
||||||
run.font.name = self.config.font['name']
|
|
||||||
run.font.size = Pt(self.config.table['font_size'])
|
|
||||||
|
|
||||||
def _process_list(self, element: Dict[str, Any]) -> None:
|
|
||||||
"""Обрабатывает элемент списка"""
|
|
||||||
items = element.get('items', [])
|
|
||||||
list_type = element.get('type', 'unordered') # 'unordered' или 'ordered'
|
|
||||||
|
|
||||||
for item in items:
|
|
||||||
p = self.document.add_paragraph(style='List GOST')
|
|
||||||
p.paragraph_format.left_indent = Cm(1.25)
|
|
||||||
p.paragraph_format.first_line_indent = Cm(-0.63) if list_type == 'ordered' else Cm(0)
|
|
||||||
|
|
||||||
if list_type == 'ordered':
|
|
||||||
# Добавляем номер вручную для упорядоченного списка
|
|
||||||
p.add_run(f"{items.index(item) + 1}. ").bold = True
|
|
||||||
|
|
||||||
p.add_run(item)
|
|
||||||
|
|
||||||
def _process_image(self, element: Dict[str, Any]) -> None:
|
|
||||||
"""Обрабатывает элемент изображения (заглушка для будущей реализации)"""
|
|
||||||
# В реальной реализации здесь будет добавление изображения
|
|
||||||
path = element.get('path', '')
|
|
||||||
width = element.get('width', None)
|
|
||||||
|
|
||||||
# Добавляем подпись
|
|
||||||
caption = self.document.add_paragraph()
|
|
||||||
caption.style = 'Caption GOST'
|
|
||||||
caption.add_run("Рисунок ").bold = True
|
|
||||||
|
|
||||||
# В реальной реализации здесь будет добавление изображения
|
|
||||||
# if path and os.path.exists(path):
|
|
||||||
# if width:
|
|
||||||
# self.document.add_picture(path, width=Cm(width))
|
|
||||||
# else:
|
|
||||||
# self.document.add_picture(path)
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def add_element(self, element_type: str, **kwargs) -> None:
|
|
||||||
"""
|
|
||||||
Абстрактный метод для добавления элемента в документ.
|
|
||||||
Должен быть реализован в дочерних классах.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
element_type: Тип элемента (heading, paragraph, table и т.д.)
|
|
||||||
**kwargs: Параметры элемента
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def save(self, filename: str) -> None:
|
|
||||||
"""
|
|
||||||
Сохраняет документ в файл
|
|
||||||
|
|
||||||
Args:
|
|
||||||
filename: Имя файла для сохранения
|
|
||||||
"""
|
|
||||||
# Добавляем настройки совместимости
|
|
||||||
try:
|
|
||||||
self.document.settings.element.xpath('./w:compat')[0].clear()
|
|
||||||
except (IndexError, AttributeError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
self.document.save(filename)
|
|
||||||
|
|
||||||
def create_title_page(self) -> None:
|
|
||||||
"""Создает титульную страницу документа"""
|
|
||||||
# Добавляем отступ сверху
|
|
||||||
for _ in range(5):
|
|
||||||
self.document.add_paragraph()
|
|
||||||
|
|
||||||
# Заголовок
|
|
||||||
title_paragraph = self.document.add_heading(level=0)
|
|
||||||
title_paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
||||||
title_run = title_paragraph.add_run(self.title)
|
|
||||||
title_run.font.size = Pt(16)
|
|
||||||
title_run.font.bold = True
|
|
||||||
title_run.font.name = self.config.font['name']
|
|
||||||
|
|
||||||
# Добавляем отступ
|
|
||||||
self.document.add_paragraph()
|
|
||||||
|
|
||||||
# Автор
|
|
||||||
author_paragraph = self.document.add_paragraph()
|
|
||||||
author_paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
||||||
author_run = author_paragraph.add_run(self.author)
|
|
||||||
author_run.font.size = Pt(self.config.font['size'])
|
|
||||||
author_run.font.name = self.config.font['name']
|
|
||||||
|
|
||||||
# Добавляем отступ снизу
|
|
||||||
for _ in range(10):
|
|
||||||
self.document.add_paragraph()
|
|
||||||
|
|
||||||
# Дата
|
|
||||||
date_paragraph = self.document.add_paragraph()
|
|
||||||
date_paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
||||||
date_run = date_paragraph.add_run("2023")
|
|
||||||
date_run.font.size = Pt(self.config.font['size'])
|
|
||||||
date_run.font.name = self.config.font['name']
|
|
||||||
|
|
||||||
# Устанавливаем титульную страницу как отдельный раздел
|
|
||||||
self.document.sections[0].start_type = 2 # Новая страница
|
|
||||||
|
|
||||||
|
|
||||||
class MarkdownGostDocument(GostDocumentCore):
|
|
||||||
"""
|
|
||||||
Расширенная реализация для обработки Markdown и преобразования в документ по ГОСТ
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, config: Optional[GostDocumentConfig] = None,
|
|
||||||
title: str = "", author: str = ""):
|
|
||||||
super().__init__(config, title, author)
|
|
||||||
self._markdown_processors = {}
|
|
||||||
self._setup_markdown_processors()
|
|
||||||
|
|
||||||
def _setup_markdown_processors(self):
|
|
||||||
"""Настраивает обработчики для различных элементов Markdown"""
|
|
||||||
# Заголовки
|
|
||||||
self.register_markdown_processor(r'#{1,6}\s+(.+)', self._process_md_heading)
|
|
||||||
# Обычный текст
|
|
||||||
self.register_markdown_processor(r'^(?!#{1,6}\s+)(?!-{3,})(?!#{1,6}$)(?!```)(?!!\[.*\]\(.*\)).+',
|
|
||||||
self._process_md_paragraph)
|
|
||||||
# Горизонтальная линия
|
|
||||||
self.register_markdown_processor(r'^-{3,}$', self._process_md_horizontal_rule)
|
|
||||||
# Списки
|
|
||||||
self.register_markdown_processor(r'^(\*|\-|\d+\.)\s+(.+)$', self._process_md_list)
|
|
||||||
# Таблицы (упрощенная обработка)
|
|
||||||
self.register_markdown_processor(r'^\|.*\|$', self._process_md_table)
|
|
||||||
# Изображения
|
|
||||||
self.register_markdown_processor(r'^!\[(.*)\]\((.*)\)$', self._process_md_image)
|
|
||||||
# Код
|
|
||||||
self.register_markdown_processor(r'^```(.*)$', self._process_md_code)
|
|
||||||
|
|
||||||
def register_markdown_processor(self, pattern: str, processor: Callable):
|
|
||||||
"""
|
|
||||||
Регистрирует обработчик для определенного паттерна Markdown
|
|
||||||
|
|
||||||
Args:
|
|
||||||
pattern: Регулярное выражение для паттерна
|
|
||||||
processor: Функция-обработчик
|
|
||||||
"""
|
|
||||||
self._markdown_processors[pattern] = processor
|
|
||||||
|
|
||||||
def _process_md_heading(self, match: re.Match, line: str) -> Dict[str, Any]:
|
|
||||||
"""Обрабатывает заголовок Markdown"""
|
|
||||||
heading_text = match.group(1).strip()
|
|
||||||
level = line.count('#')
|
|
||||||
# Ограничиваем уровень заголовка от 1 до 3 (согласно ГОСТ)
|
|
||||||
level = min(max(level, 1), 3)
|
|
||||||
return {'type': 'heading', 'level': level, 'text': heading_text}
|
|
||||||
|
|
||||||
def _process_md_paragraph(self, match: re.Match, line: str) -> Dict[str, Any]:
|
|
||||||
"""Обрабатывает обычный текст Markdown"""
|
|
||||||
return {'type': 'paragraph', 'text': line.strip()}
|
|
||||||
|
|
||||||
def _process_md_horizontal_rule(self, match: re.Match, line: str) -> Dict[str, Any]:
|
|
||||||
"""Обрабатывает горизонтальную линию Markdown"""
|
|
||||||
# В ГОСТ горизонтальные линии не используются, поэтому просто пропускаем
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _process_md_list(self, match: re.Match, line: str) -> Dict[str, Any]:
|
|
||||||
"""Обрабатывает список Markdown"""
|
|
||||||
items = [line.strip()]
|
|
||||||
list_type = 'ordered' if re.match(r'^\d+\.', line) else 'unordered'
|
|
||||||
return {'type': 'list', 'items': items, 'type': list_type}
|
|
||||||
|
|
||||||
def _process_md_table(self, match: re.Match, line: str) -> Dict[str, Any]:
|
|
||||||
"""Обрабатывает таблицу Markdown (заглушка)"""
|
|
||||||
# Полная обработка таблиц требует анализа нескольких строк
|
|
||||||
return None # Будет обработано в полной реализации
|
|
||||||
|
|
||||||
def _process_md_image(self, match: re.Match, line: str) -> Dict[str, Any]:
|
|
||||||
"""Обрабатывает изображение Markdown"""
|
|
||||||
alt_text = match.group(1)
|
|
||||||
image_path = match.group(2)
|
|
||||||
return {'type': 'image', 'path': image_path, 'alt': alt_text}
|
|
||||||
|
|
||||||
def _process_md_code(self, match: re.Match, line: str) -> Dict[str, Any]:
|
|
||||||
"""Обрабатывает блок кода Markdown"""
|
|
||||||
# Код в ГОСТ обычно оформляется как отдельный раздел или сноска
|
|
||||||
return {'type': 'paragraph', 'text': f"Код: {line}", 'style': 'Code'}
|
|
||||||
|
|
||||||
def add_element(self, element_type: str, **kwargs) -> None:
|
|
||||||
"""
|
|
||||||
Добавляет элемент в документ согласно ГОСТ
|
|
||||||
|
|
||||||
Args:
|
|
||||||
element_type: Тип элемента (heading, paragraph, table и т.д.)
|
|
||||||
**kwargs: Параметры элемента
|
|
||||||
"""
|
|
||||||
processor = self.get_processor(element_type)
|
|
||||||
if processor:
|
|
||||||
element = {'type': element_type, **kwargs}
|
|
||||||
processor(element)
|
|
||||||
|
|
||||||
def process_markdown(self, markdown_text: str) -> None:
|
|
||||||
"""
|
|
||||||
Обрабатывает Markdown текст и добавляет элементы в документ
|
|
||||||
|
|
||||||
Args:
|
|
||||||
markdown_text: Текст в формате Markdown
|
|
||||||
"""
|
|
||||||
lines = markdown_text.split('\n')
|
|
||||||
i = 0
|
|
||||||
while i < len(lines):
|
|
||||||
line = lines[i].strip()
|
|
||||||
if not line:
|
|
||||||
i += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
processed = False
|
|
||||||
for pattern, processor in self._markdown_processors.items():
|
|
||||||
match = re.match(pattern, line)
|
|
||||||
if match:
|
|
||||||
element = processor(match, line)
|
|
||||||
if element:
|
|
||||||
self.add_element(element['type'], **{k: v for k, v in element.items() if k != 'type'})
|
|
||||||
processed = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if not processed:
|
|
||||||
# Если строка не соответствует ни одному паттерну, обрабатываем как обычный текст
|
|
||||||
self.add_element('paragraph', text=line)
|
|
||||||
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
def process_markdown_file(self, file_path: str) -> None:
|
|
||||||
"""
|
|
||||||
Обрабатывает файл Markdown и добавляет элементы в документ
|
|
||||||
|
|
||||||
Args:
|
|
||||||
file_path: Путь к файлу Markdown
|
|
||||||
"""
|
|
||||||
with open(file_path, 'r', encoding='utf-8') as f:
|
|
||||||
markdown_text = f.read()
|
|
||||||
self.process_markdown(markdown_text)
|
|
||||||
|
|
||||||
|
|
||||||
# Пример использования
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# Создаем кастомную конфигурацию (можно оставить стандартную)
|
|
||||||
custom_config = GostDocumentConfig(
|
|
||||||
margins={
|
|
||||||
'top': 2.0,
|
|
||||||
'bottom': 2.0,
|
|
||||||
'left': 3.0,
|
|
||||||
'right': 1.5
|
|
||||||
},
|
|
||||||
font={
|
|
||||||
'name': 'Times New Roman',
|
|
||||||
'size': 14
|
|
||||||
},
|
|
||||||
line_spacing=1.5,
|
|
||||||
heading_spacing={
|
|
||||||
1: {'before': 24, 'after': 12},
|
|
||||||
2: {'before': 24, 'after': 6},
|
|
||||||
3: {'before': 18, 'after': 6}
|
|
||||||
},
|
|
||||||
heading_styles={
|
|
||||||
1: {'size': 16, 'bold': True, 'all_caps': True, 'align': 'center'},
|
|
||||||
2: {'size': 14, 'bold': True, 'all_caps': False, 'align': 'left'},
|
|
||||||
3: {'size': 14, 'bold': True, 'italic': True, 'all_caps': False, 'align': 'left'}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Создаем документ с поддержкой Markdown
|
|
||||||
doc = MarkdownGostDocument(
|
|
||||||
config=custom_config,
|
|
||||||
title="Отчет о научно-исследовательской работе",
|
|
||||||
author="Иванов И.И., группа 123"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Добавляем титульную страницу
|
|
||||||
doc.create_title_page()
|
|
||||||
|
|
||||||
# Пример обработки Markdown
|
|
||||||
markdown_example = """
|
|
||||||
## Практическая работа 6 (часть 2)
|
|
||||||
**Тема. Нелинейные структуры данных. Бинарное дерево.**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 1. Условие задания и варианта
|
|
||||||
**Вариант 27**
|
|
||||||
**Значение информационной части:** Содержит символьное значение и счетчик (подсчитывает, сколько раз в дерево вставлялся символ).
|
|
||||||
**Операции варианта:**
|
|
||||||
1. Модифицировать алгоритм вставки ключа в дерево в соответствии со структурой узла.
|
|
||||||
2. Определить символ, который встретился большее количество раз.
|
|
||||||
3. Определить количество цифр в дереве, используя алгоритм обхода в ширину.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. Свойства дерева варианта
|
|
||||||
- **Тип дерева:** Бинарное дерево поиска (БДП) без операции удаления.
|
|
||||||
- **Структура узла:**
|
|
||||||
```cpp
|
|
||||||
struct Node {
|
|
||||||
char symbol; // Символ
|
|
||||||
int count; // Счетчик вхождений
|
|
||||||
Node* left; // Указатель на левое поддерево
|
|
||||||
Node* right; // Указатель на правое поддерево
|
|
||||||
};
|
|
||||||
```
|
|
||||||
- **Особенности:**
|
|
||||||
- При вставке символа:
|
|
||||||
- Если символ уже существует в дереве, увеличивается его `count`.
|
|
||||||
- Иначе создается новый узел с `count = 1`.
|
|
||||||
- Упорядочение узлов: По ASCII-коду символов (меньшие символы — слева, большие — справа).
|
|
||||||
- Операция удаления не реализована (требования варианта 24–30).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. Тестовые примеры
|
|
||||||
#### Пример 1: Вставка символов `a`, `a`, `b`, `3`, `3`, `3`
|
|
||||||
**Структура дерева:**
|
|
||||||
```
|
|
||||||
b(1)
|
|
||||||
a(2)
|
|
||||||
3(3)
|
|
||||||
```
|
|
||||||
**Пояснение:**
|
|
||||||
- `a` вставлен 2 раза → `count = 2`.
|
|
||||||
- `3` вставлен 3 раза → `count = 3` (самый частый символ).
|
|
||||||
- Количество цифр: 3 (только символ `3`).
|
|
||||||
|
|
||||||
#### Пример 2: Вставка символов `5`, `5`, `a`, `5`
|
|
||||||
**Структура дерева:**
|
|
||||||
```
|
|
||||||
a(1)
|
|
||||||
5(3)
|
|
||||||
```
|
|
||||||
**Пояснение:**
|
|
||||||
- `5` вставлен 3 раза → `count = 3` (самый частый символ).
|
|
||||||
- Количество цифр: 3.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4. Описание функций
|
|
||||||
|
|
||||||
#### 4.1. Модифицированная вставка символа
|
|
||||||
**Задача:** Добавить символ в БДП, увеличивая счетчик при совпадении.
|
|
||||||
**Предусловие:** Дерево инициализировано.
|
|
||||||
**Постусловие:** Символ добавлен или обновлен в дереве.
|
|
||||||
**Прототип:**
|
|
||||||
```cpp
|
|
||||||
void BinaryTree::insert(char symbol);
|
|
||||||
```
|
|
||||||
**Алгоритм:**
|
|
||||||
1. Начать с корня.
|
|
||||||
2. Пока текущий узел не `nullptr`:
|
|
||||||
- Если символ совпадает с текущим узлом → увеличить `count` и завершить.
|
|
||||||
- Если символ меньше текущего → перейти в левое поддерево.
|
|
||||||
- Иначе → перейти в правое поддерево.
|
|
||||||
3. Если узел не найден → создать новый узел с `count = 1`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 4.2. Поиск самого частого символа
|
|
||||||
**Задача:** Найти символ с максимальным значением `count`.
|
|
||||||
**Предусловие:** Дерево не пустое.
|
|
||||||
**Постусловие:** Возвращен символ и его счетчик.
|
|
||||||
**Прототип:**
|
|
||||||
```cpp
|
|
||||||
std::pair<char, int> BinaryTree::findMostFrequent();
|
|
||||||
```
|
|
||||||
**Алгоритм:**
|
|
||||||
1. Инициализировать `maxCount = 0`, `maxSymbol = '\\0'`.
|
|
||||||
2. Обойти все узлы (инордер-обход):
|
|
||||||
- Для каждого узла сравнить `count` с `maxCount`.
|
|
||||||
- Если `count > maxCount` → обновить `maxSymbol` и `maxCount`.
|
|
||||||
3. Вернуть пару `(maxSymbol, maxCount)`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 4.3. Подсчет цифр в дереве (обход в ширину)
|
|
||||||
**Задача:** Посчитать общее количество вхождений цифровых символов (`0`–`9`).
|
|
||||||
**Предусловие:** Дерево инициализировано.
|
|
||||||
**Постусловие:** Возвращено число — сумма `count` для всех цифровых символов.
|
|
||||||
**Прототип:**
|
|
||||||
```cpp
|
|
||||||
int BinaryTree::countDigits();
|
|
||||||
```
|
|
||||||
**Алгоритм:**
|
|
||||||
1. Инициализировать очередь и счетчик `total = 0`.
|
|
||||||
2. Поместить корень в очередь.
|
|
||||||
3. Пока очередь не пуста:
|
|
||||||
- Извлечь узел из очереди.
|
|
||||||
- Если символ узла — цифра (`'0'`–`'9'`) → добавить `count` к `total`.
|
|
||||||
- Поместить левый и правый дочерние узлы в очередь.
|
|
||||||
4. Вернуть `total`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 4.4. Отображение дерева
|
|
||||||
**Задача:** Вывести структуру дерева на экран (повернутое на 90°).
|
|
||||||
**Предусловие:** Дерево инициализировано.
|
|
||||||
**Постусловие:** Дерево отображено в консоли.
|
|
||||||
**Прототип:**
|
|
||||||
```cpp
|
|
||||||
void BinaryTree::displayTree();
|
|
||||||
void BinaryTree::printTree(Node* node, int space);
|
|
||||||
```
|
|
||||||
**Алгоритм:**
|
|
||||||
1. Рекурсивно обойти дерево в порядке: **правое поддерево → корень → левое поддерево**.
|
|
||||||
2. Для каждого уровня увеличивать отступ (на 10 пробелов).
|
|
||||||
3. Вывести символ и `count` с текущим отступом.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 5. Код программы
|
|
||||||
```cpp
|
|
||||||
#include <iostream>
|
|
||||||
#include <stack>
|
|
||||||
#include <queue>
|
|
||||||
using namespace std;
|
|
||||||
|
|
||||||
struct Node {
|
|
||||||
char symbol;
|
|
||||||
int count;
|
|
||||||
Node* left;
|
|
||||||
Node* right;
|
|
||||||
Node(char s) : symbol(s), count(1), left(nullptr), right(nullptr) {}
|
|
||||||
};
|
|
||||||
|
|
||||||
class BinaryTree {
|
|
||||||
private:
|
|
||||||
Node* root;
|
|
||||||
|
|
||||||
void printTree(Node* node, int space) {
|
|
||||||
if (node == nullptr) return;
|
|
||||||
space += 10;
|
|
||||||
printTree(node->right, space);
|
|
||||||
cout << endl;
|
|
||||||
for (int i = 10; i < space; i++) cout << " ";
|
|
||||||
cout << node->symbol << "(" << node->count << ")";
|
|
||||||
printTree(node->left, space);
|
|
||||||
}
|
|
||||||
|
|
||||||
public:
|
|
||||||
BinaryTree() : root(nullptr) {}
|
|
||||||
|
|
||||||
void insert(char symbol) {
|
|
||||||
Node** current = &root;
|
|
||||||
while (*current != nullptr) {
|
|
||||||
if (symbol == (*current)->symbol) {
|
|
||||||
(*current)->count++;
|
|
||||||
return;
|
|
||||||
} else if (symbol < (*current)->symbol) {
|
|
||||||
current = &((*current)->left);
|
|
||||||
} else {
|
|
||||||
current = &((*current)->right);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*current = new Node(symbol);
|
|
||||||
}
|
|
||||||
|
|
||||||
pair<char, int> findMostFrequent() {
|
|
||||||
if (root == nullptr) return make_pair('\\0', 0);
|
|
||||||
char maxSymbol = '\\0';
|
|
||||||
int maxCount = 0;
|
|
||||||
stack<Node*> s;
|
|
||||||
Node* current = root;
|
|
||||||
while (current != nullptr || !s.empty()) {
|
|
||||||
while (current != nullptr) {
|
|
||||||
s.push(current);
|
|
||||||
current = current->left;
|
|
||||||
}
|
|
||||||
current = s.top();
|
|
||||||
s.pop();
|
|
||||||
if (current->count > maxCount) {
|
|
||||||
maxCount = current->count;
|
|
||||||
maxSymbol = current->symbol;
|
|
||||||
}
|
|
||||||
current = current->right;
|
|
||||||
}
|
|
||||||
return make_pair(maxSymbol, maxCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
int countDigits() {
|
|
||||||
if (root == nullptr) return 0;
|
|
||||||
int total = 0;
|
|
||||||
queue<Node*> q;
|
|
||||||
q.push(root);
|
|
||||||
while (!q.empty()) {
|
|
||||||
Node* current = q.front();
|
|
||||||
q.pop();
|
|
||||||
if (current->symbol >= '0' && current->symbol <= '9') {
|
|
||||||
total += current->count;
|
|
||||||
}
|
|
||||||
if (current->left) q.push(current->left);
|
|
||||||
if (current->right) q.push(current->right);
|
|
||||||
}
|
|
||||||
return total;
|
|
||||||
}
|
|
||||||
|
|
||||||
void displayTree() {
|
|
||||||
printTree(root, 0);
|
|
||||||
cout << "\n\n";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
BinaryTree tree;
|
|
||||||
int choice;
|
|
||||||
char symbol;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
cout << "\n1. Insert symbol\n";
|
|
||||||
cout << "2. Display tree\n";
|
|
||||||
cout << "3. Find most frequent symbol\n";
|
|
||||||
cout << "4. Count digits\n";
|
|
||||||
cout << "5. Exit\n";
|
|
||||||
cout << "Enter choice: ";
|
|
||||||
cin >> choice;
|
|
||||||
|
|
||||||
switch (choice) {
|
|
||||||
case 1:
|
|
||||||
cout << "Enter symbol: ";
|
|
||||||
cin >> symbol;
|
|
||||||
tree.insert(symbol);
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
cout << "Tree structure:\n";
|
|
||||||
tree.displayTree();
|
|
||||||
break;
|
|
||||||
case 3: {
|
|
||||||
auto result = tree.findMostFrequent();
|
|
||||||
if (result.second == 0) {
|
|
||||||
cout << "Tree is empty.\n";
|
|
||||||
} else {
|
|
||||||
cout << "Most frequent symbol: " << result.first
|
|
||||||
<< " (count: " << result.second << ")\n";
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 4:
|
|
||||||
cout << "Number of digits: " << tree.countDigits() << endl;
|
|
||||||
break;
|
|
||||||
case 5:
|
|
||||||
return 0;
|
|
||||||
default:
|
|
||||||
cout << "Invalid choice.\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 6. Скриншоты тестирования
|
|
||||||
#### Тест 1: Вставка `a`, `a`, `b`, `3`, `3`, `3`
|
|
||||||

|
|
||||||
- **Вывод дерева:** Корректно отображена структура с `3(3)` как самым частым символом.
|
|
||||||
- **Количество цифр:** 3 (символ `3` вставлен 3 раза).
|
|
||||||
|
|
||||||
#### Тест 2: Вставка `5`, `5`, `a`, `5`
|
|
||||||

|
|
||||||
- **Самый частый символ:** `5` (count=3).
|
|
||||||
- **Количество цифр:** 3.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 7. Ответы на контрольные вопросы
|
|
||||||
1. **Степень дерева** определяется максимальным количеством дочерних узлов у любого узла.
|
|
||||||
2. **Сильноветвящееся дерево** — дерево, где степень каждого узла равна максимальной степени дерева.
|
|
||||||
3. **Путь в дереве** — последовательность узлов от корня к целевому узлу.
|
|
||||||
4. **Длина пути** — количество ребер в пути (на 1 меньше количества узлов).
|
|
||||||
5. **Степень бинарного дерева** равна 2 (максимум 2 дочерних узла на узел).
|
|
||||||
6. **Пустое дерево** допустимо (корень = `nullptr`).
|
|
||||||
7. **Бинарное дерево** — дерево, где каждый узел имеет не более 2 дочерних узлов.
|
|
||||||
8. **Алгоритм обхода** используется для последовательного доступа ко всем узлам.
|
|
||||||
9. **Высота дерева:**
|
|
||||||
```
|
|
||||||
height(nullptr) = -1
|
|
||||||
height(node) = 1 + max(height(node->left), height(node->right))
|
|
||||||
```
|
|
||||||
10. **Дерево по таблице:**
|
|
||||||

|
|
||||||
11. **Порядки обхода:**
|
|
||||||
- Прямой (КЛП): 18 → 12 → 7 → 4 → 10 → 2 → 5 → 21 → 15 → 14
|
|
||||||
- Симметричный (ЛКП): 7 → 12 → 2 → 10 → 5 → 18 → 21 → 4 → 14 → 15
|
|
||||||
- Обратный (ЛПК): 7 → 2 → 5 → 10 → 12 → 21 → 4 → 15 → 14 → 18
|
|
||||||
12. **Обход в ширину** использует **очередь**.
|
|
||||||
13. **Пример обхода в ширину:** 18 → 12 → 14 → 7 → 10 → 15 → 4 → 2 → 5 → 21.
|
|
||||||
14. **Не рекурсивный обход в глубину** использует **стек**.
|
|
||||||
15. **Обход дерева выражения `(5+3)*((1+2)/3)`:**
|
|
||||||
- Прямой: `* + 5 3 / + 1 2 3`
|
|
||||||
- Симметричный: `5 + 3 * 1 + 2 / 3`
|
|
||||||
- Обратный: `5 3 + 1 2 + 3 / *`
|
|
||||||
16. **Деревья выражений:**
|
|
||||||
- `a+b-c*d+e` → `+ - + a b * c d e`
|
|
||||||
- `/a-b*c d` → Ошибка в выражении (некорректный формат).
|
|
||||||
- `a b c d / - *` → `* a - b / c d`
|
|
||||||
- `* - / + a b c d e` → `* - / + a b c d e`
|
|
||||||
17. **Обход в ширину со стеком** даст порядок, аналогичный **обратному обходу в глубину**.
|
|
||||||
18. **БДП для симметричного обхода `40 45 46 50 65 70 75`:**
|
|
||||||

|
|
||||||
19. **Исходное дерево для прямого обхода `50 45 35 15 40 46 65 75 70`:**
|
|
||||||

|
|
||||||
20. **БДП после вставки `1, 48, 75, 100`:**
|
|
||||||

|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Вывод:** Реализованы все операции для бинарного дерева поиска с модифицированной структурой узла. Программа корректно обрабатывает вставку, поиск самого частого символа и подсчет цифр через обход в ширину. Тесты подтверждают работоспособность алгоритмов..
|
|
||||||
"""
|
|
||||||
# Обрабатываем Markdown
|
|
||||||
doc.process_markdown(markdown_example)
|
|
||||||
|
|
||||||
# Сохраняем документ
|
|
||||||
doc.save("./asdotchet_po_GOST.docx")
|
|
||||||
print("Документ успешно создан и сохранен как 'otchet_po_GOST.docx'")
|
|
||||||
+102
-151
@@ -11,13 +11,23 @@ from pathlib import Path
|
|||||||
from md2gost.extended_markdown import markdown
|
from md2gost.extended_markdown import markdown
|
||||||
from md2gost.diagram_renderer import DIAGRAM_LANGS
|
from md2gost.diagram_renderer import DIAGRAM_LANGS
|
||||||
from md2gost.profiles import preprocess_markdown
|
from md2gost.profiles import preprocess_markdown
|
||||||
|
from mdexport import (
|
||||||
|
BlockVisitor,
|
||||||
|
InlineRenderer,
|
||||||
|
caption_tuple,
|
||||||
|
is_contents_heading,
|
||||||
|
parse_image_caption,
|
||||||
|
reference_full_name,
|
||||||
|
render_inline,
|
||||||
|
walk_blocks,
|
||||||
|
)
|
||||||
|
|
||||||
from .escape import escape_text, xml_id
|
from .escape import escape_text, xml_id
|
||||||
from .styles import FODT_NS, automatic_styles_xml, styles_xml
|
from .styles import FODT_NS, automatic_styles_xml, styles_xml
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class EmitState:
|
class EmitState(BlockVisitor):
|
||||||
body: list[str] = field(default_factory=list)
|
body: list[str] = field(default_factory=list)
|
||||||
pending_caption: tuple[str | None, str | None, bool] | None = None
|
pending_caption: tuple[str | None, str | None, bool] | None = None
|
||||||
biblio: list[tuple[str, str]] = field(default_factory=list)
|
biblio: list[tuple[str, str]] = field(default_factory=list)
|
||||||
@@ -31,55 +41,75 @@ class EmitState:
|
|||||||
def w(self, s: str = "") -> None:
|
def w(self, s: str = "") -> None:
|
||||||
self.body.append(s)
|
self.body.append(s)
|
||||||
|
|
||||||
|
def on_heading(self, element) -> None:
|
||||||
|
_emit_heading(self, element)
|
||||||
|
|
||||||
|
def on_table(self, element, caption_id: str | None, caption_text: str | None) -> None:
|
||||||
|
_emit_table(self, element, caption_id, caption_text)
|
||||||
|
|
||||||
|
def on_code(self, element, lang: str) -> None:
|
||||||
|
_emit_code(self, element, lang)
|
||||||
|
|
||||||
|
def on_list(self, element) -> None:
|
||||||
|
_emit_list(self, element)
|
||||||
|
|
||||||
|
def on_equation(self, element) -> None:
|
||||||
|
_emit_equation(self, element)
|
||||||
|
|
||||||
|
def on_image(self, image) -> None:
|
||||||
|
_emit_image(self, image)
|
||||||
|
|
||||||
|
def on_body_text(self, children) -> None:
|
||||||
|
text = _inline(children, self).strip()
|
||||||
|
if text:
|
||||||
|
self.w(_p("Standard", text))
|
||||||
|
|
||||||
|
def on_quote(self, element) -> None:
|
||||||
|
for child in element.children:
|
||||||
|
if type(child).__name__ == "Paragraph":
|
||||||
|
self.w(_p("Standard", _inline(child.children, self)))
|
||||||
|
|
||||||
|
def on_other(self, element) -> None:
|
||||||
|
self.on_body_text(element.children)
|
||||||
|
|
||||||
|
|
||||||
|
class _FodtInline(InlineRenderer):
|
||||||
|
def __init__(self, st: EmitState | None):
|
||||||
|
self.st = st
|
||||||
|
|
||||||
|
def escape(self, text: str) -> str:
|
||||||
|
return escape_text(text)
|
||||||
|
|
||||||
|
def code_span(self, node) -> str:
|
||||||
|
return f'<text:span text:style-name="Tcode">{escape_text(node.children)}</text:span>'
|
||||||
|
|
||||||
|
def emphasis(self, inner: str) -> str:
|
||||||
|
return f'<text:span text:style-name="Titalic">{inner}</text:span>'
|
||||||
|
|
||||||
|
def strong_emphasis(self, inner: str) -> str:
|
||||||
|
return f'<text:span text:style-name="Tbold">{inner}</text:span>'
|
||||||
|
|
||||||
|
def link(self, node, inner: str) -> str:
|
||||||
|
href = escape_text(node.dest)
|
||||||
|
return f'<text:a xlink:type="simple" xlink:href="{href}">{inner}</text:a>'
|
||||||
|
|
||||||
|
def reference(self, node) -> str:
|
||||||
|
key = f"{node.type.lower()}:{node.name}"
|
||||||
|
num = (self.st.labels.get(key) if self.st else None) or "?"
|
||||||
|
return escape_text(f"{reference_full_name(node.type)} {num}")
|
||||||
|
|
||||||
|
def inline_equation(self, node) -> str:
|
||||||
|
return (
|
||||||
|
f'<text:span text:style-name="Tcode">'
|
||||||
|
f"{escape_text(node.latex_equation)}</text:span>"
|
||||||
|
)
|
||||||
|
|
||||||
|
def line_break(self) -> str:
|
||||||
|
return "<text:line-break/>"
|
||||||
|
|
||||||
|
|
||||||
def _inline(children, st: EmitState | None = None) -> str:
|
def _inline(children, st: EmitState | None = None) -> str:
|
||||||
parts: list[str] = []
|
return render_inline(children, _FodtInline(st))
|
||||||
for child in children or []:
|
|
||||||
t = type(child).__name__
|
|
||||||
if t in ("RawText", "Literal"):
|
|
||||||
parts.append(escape_text(getattr(child, "children", "") or ""))
|
|
||||||
elif t == "CodeSpan":
|
|
||||||
parts.append(
|
|
||||||
f'<text:span text:style-name="Tcode">{escape_text(child.children)}</text:span>'
|
|
||||||
)
|
|
||||||
elif t == "Emphasis":
|
|
||||||
parts.append(
|
|
||||||
f'<text:span text:style-name="Titalic">{_inline(child.children, st)}</text:span>'
|
|
||||||
)
|
|
||||||
elif t == "StrongEmphasis":
|
|
||||||
parts.append(
|
|
||||||
f'<text:span text:style-name="Tbold">{_inline(child.children, st)}</text:span>'
|
|
||||||
)
|
|
||||||
elif t == "Link":
|
|
||||||
href = escape_text(child.dest)
|
|
||||||
parts.append(
|
|
||||||
f'<text:a xlink:type="simple" xlink:href="{href}">'
|
|
||||||
f"{_inline(child.children, st)}</text:a>"
|
|
||||||
)
|
|
||||||
elif t == "Reference":
|
|
||||||
full = {
|
|
||||||
"рисунок": "Рисунок",
|
|
||||||
"таблица": "Таблица",
|
|
||||||
"листинг": "Листинг",
|
|
||||||
"формула": "формула",
|
|
||||||
}.get(child.type.lower(), child.type)
|
|
||||||
key = f"{child.type.lower()}:{child.name}"
|
|
||||||
num = (st.labels.get(key) if st else None) or "?"
|
|
||||||
parts.append(escape_text(f"{full} {num}"))
|
|
||||||
elif t == "InlineEquation":
|
|
||||||
parts.append(
|
|
||||||
f'<text:span text:style-name="Tcode">'
|
|
||||||
f"{escape_text(child.latex_equation)}</text:span>"
|
|
||||||
)
|
|
||||||
elif t == "Image":
|
|
||||||
parts.append("")
|
|
||||||
elif t == "LineBreak":
|
|
||||||
parts.append("<text:line-break/>")
|
|
||||||
elif hasattr(child, "children"):
|
|
||||||
parts.append(_inline(child.children, st))
|
|
||||||
else:
|
|
||||||
parts.append(escape_text(str(child)))
|
|
||||||
return "".join(parts)
|
|
||||||
|
|
||||||
|
|
||||||
def _plain_inline(children) -> str:
|
def _plain_inline(children) -> str:
|
||||||
@@ -109,10 +139,9 @@ def _h(level: int, content: str) -> str:
|
|||||||
def _emit_heading(st: EmitState, heading) -> None:
|
def _emit_heading(st: EmitState, heading) -> None:
|
||||||
text = _inline(heading.children, st).strip()
|
text = _inline(heading.children, st).strip()
|
||||||
plain = _plain_inline(heading.children).strip()
|
plain = _plain_inline(heading.children).strip()
|
||||||
upper = plain.upper()
|
|
||||||
level = min(int(heading.level), 3)
|
level = min(int(heading.level), 3)
|
||||||
|
|
||||||
if upper == "СОДЕРЖАНИЕ" or upper.startswith("СОДЕРЖАНИЕ"):
|
if is_contents_heading(plain):
|
||||||
st.w(_p("Centered", escape_text("СОДЕРЖАНИЕ")))
|
st.w(_p("Centered", escape_text("СОДЕРЖАНИЕ")))
|
||||||
st.w(
|
st.w(
|
||||||
'<text:table-of-content text:style-name="Standard" text:protected="false" '
|
'<text:table-of-content text:style-name="Standard" text:protected="false" '
|
||||||
@@ -132,32 +161,6 @@ def _emit_heading(st: EmitState, heading) -> None:
|
|||||||
st.w(_h(level, text))
|
st.w(_h(level, text))
|
||||||
|
|
||||||
|
|
||||||
def _emit_paragraph(st: EmitState, para) -> None:
|
|
||||||
text_probe = ""
|
|
||||||
for ch in para.children or []:
|
|
||||||
if type(ch).__name__ in ("RawText", "Literal"):
|
|
||||||
text_probe += ch.children or ""
|
|
||||||
m = re.match(r"^\[([^\]]+)\]:\s*(.+)$", text_probe.strip())
|
|
||||||
if m:
|
|
||||||
st.biblio.append((m.group(1), m.group(2).strip()))
|
|
||||||
return
|
|
||||||
|
|
||||||
has_image = any(type(c).__name__ == "Image" for c in (para.children or []))
|
|
||||||
if has_image:
|
|
||||||
for c in para.children:
|
|
||||||
if type(c).__name__ == "Image":
|
|
||||||
_emit_image(st, c)
|
|
||||||
others = [c for c in para.children if type(c).__name__ != "Image"]
|
|
||||||
t = _inline(others, st).strip()
|
|
||||||
if t:
|
|
||||||
st.w(_p("Standard", t))
|
|
||||||
return
|
|
||||||
|
|
||||||
raw = _inline(para.children, st).strip()
|
|
||||||
if raw:
|
|
||||||
st.w(_p("Standard", raw))
|
|
||||||
|
|
||||||
|
|
||||||
def _mime_for(path: Path) -> str:
|
def _mime_for(path: Path) -> str:
|
||||||
ext = path.suffix.lower()
|
ext = path.suffix.lower()
|
||||||
return {
|
return {
|
||||||
@@ -173,13 +176,7 @@ def _mime_for(path: Path) -> str:
|
|||||||
|
|
||||||
def _emit_image(st: EmitState, img) -> None:
|
def _emit_image(st: EmitState, img) -> None:
|
||||||
dest = img.dest
|
dest = img.dest
|
||||||
title = getattr(img, "title", None) or ""
|
caption_id, caption_text = parse_image_caption(img, getattr(img, "unique_name", None))
|
||||||
caption_id = getattr(img, "unique_name", None)
|
|
||||||
caption_text = title
|
|
||||||
if title.startswith("%"):
|
|
||||||
parts = title[1:].split(None, 1)
|
|
||||||
caption_id = parts[0]
|
|
||||||
caption_text = parts[1] if len(parts) > 1 else ""
|
|
||||||
|
|
||||||
if st.pending_caption and not caption_text:
|
if st.pending_caption and not caption_text:
|
||||||
caption_id = caption_id or st.pending_caption[0]
|
caption_id = caption_id or st.pending_caption[0]
|
||||||
@@ -345,10 +342,7 @@ def _emit_table_row(
|
|||||||
|
|
||||||
def _emit_code(st: EmitState, block, lang: str) -> None:
|
def _emit_code(st: EmitState, block, lang: str) -> None:
|
||||||
source = block.children[0].children if block.children else ""
|
source = block.children[0].children if block.children else ""
|
||||||
cap_id, cap_text, _with_listing = (None, None, False)
|
cap_id, cap_text, _with_listing = st.take_caption()
|
||||||
if st.pending_caption:
|
|
||||||
cap_id, cap_text, _with_listing = st.pending_caption
|
|
||||||
st.pending_caption = None
|
|
||||||
|
|
||||||
lang_l = (lang or "").strip().lower()
|
lang_l = (lang or "").strip().lower()
|
||||||
if lang_l in DIAGRAM_LANGS:
|
if lang_l in DIAGRAM_LANGS:
|
||||||
@@ -402,10 +396,7 @@ def _emit_list(st: EmitState, lst) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _emit_equation(st: EmitState, eq) -> None:
|
def _emit_equation(st: EmitState, eq) -> None:
|
||||||
cap_id = None
|
cap_id = st.take_caption()[0]
|
||||||
if st.pending_caption:
|
|
||||||
cap_id = st.pending_caption[0]
|
|
||||||
st.pending_caption = None
|
|
||||||
latex = eq.latex_equation.strip()
|
latex = eq.latex_equation.strip()
|
||||||
bookmark = ""
|
bookmark = ""
|
||||||
if cap_id:
|
if cap_id:
|
||||||
@@ -413,20 +404,16 @@ def _emit_equation(st: EmitState, eq) -> None:
|
|||||||
st.w(_p("Centered", bookmark + f'<text:span text:style-name="Tcode">{escape_text(latex)}</text:span>'))
|
st.w(_p("Centered", bookmark + f'<text:span text:style-name="Tcode">{escape_text(latex)}</text:span>'))
|
||||||
|
|
||||||
|
|
||||||
def emit_body(md_text: str, *, work_dir: Path, emdash_to_hyphen: bool = False) -> EmitState:
|
def _assign_numbers(st: EmitState, children) -> None:
|
||||||
st = EmitState(work_dir=work_dir)
|
"""
|
||||||
pre = preprocess_markdown(md_text, emdash_to_hyphen=emdash_to_hyphen)
|
Number captioned objects up front so `@Тип:id` references resolve even when
|
||||||
# Two-pass: first collect captions/labels roughly by walking once for numbers,
|
they appear before the object they point at.
|
||||||
# then emit. Simple approach: single pass (refs before definition show "?").
|
"""
|
||||||
# Better: first pass assign numbers, second emit.
|
|
||||||
doc = markdown.parse(pre)
|
|
||||||
|
|
||||||
# Pass 1 — assign numbers for captions attached to tables/images/listings
|
|
||||||
pending = None
|
pending = None
|
||||||
for el in doc.children:
|
for el in children:
|
||||||
name = type(el).__name__
|
name = type(el).__name__
|
||||||
if name == "Caption":
|
if name == "Caption":
|
||||||
pending = (el.unique_name, el.text, getattr(el, "with_listing", False))
|
pending = caption_tuple(el)
|
||||||
continue
|
continue
|
||||||
if name == "Table" and pending:
|
if name == "Table" and pending:
|
||||||
st.tab_n += 1
|
st.tab_n += 1
|
||||||
@@ -441,66 +428,30 @@ def emit_body(md_text: str, *, work_dir: Path, emdash_to_hyphen: bool = False) -
|
|||||||
for c in el.children or []:
|
for c in el.children or []:
|
||||||
if type(c).__name__ == "Image":
|
if type(c).__name__ == "Image":
|
||||||
st.fig_n += 1
|
st.fig_n += 1
|
||||||
cid = pending[0]
|
cid = parse_image_caption(c, pending[0])[0]
|
||||||
title = getattr(c, "title", "") or ""
|
|
||||||
if title.startswith("%"):
|
|
||||||
cid = title[1:].split(None, 1)[0]
|
|
||||||
st.labels[f"рисунок:{cid}"] = str(st.fig_n)
|
st.labels[f"рисунок:{cid}"] = str(st.fig_n)
|
||||||
pending = None
|
pending = None
|
||||||
elif name == "Equation" and pending:
|
elif name == "Equation" and pending:
|
||||||
pending = None
|
pending = None
|
||||||
|
|
||||||
|
|
||||||
|
def emit_body(md_text: str, *, work_dir: Path, emdash_to_hyphen: bool = False) -> EmitState:
|
||||||
|
st = EmitState(work_dir=work_dir)
|
||||||
|
pre = preprocess_markdown(md_text, emdash_to_hyphen=emdash_to_hyphen)
|
||||||
|
# Two-pass: first collect captions/labels roughly by walking once for numbers,
|
||||||
|
# then emit. Simple approach: single pass (refs before definition show "?").
|
||||||
|
# Better: first pass assign numbers, second emit.
|
||||||
|
doc = markdown.parse(pre)
|
||||||
|
|
||||||
|
_assign_numbers(st, doc.children)
|
||||||
|
|
||||||
# reset counters for emit
|
# reset counters for emit
|
||||||
st.fig_n = 0
|
st.fig_n = 0
|
||||||
st.tab_n = 0
|
st.tab_n = 0
|
||||||
st.lst_n = 0
|
st.lst_n = 0
|
||||||
st.pending_caption = None
|
st.pending_caption = None
|
||||||
|
|
||||||
for el in doc.children:
|
walk_blocks(doc.children, st)
|
||||||
name = type(el).__name__
|
|
||||||
if name == "BlankLine":
|
|
||||||
continue
|
|
||||||
if name == "Caption":
|
|
||||||
st.pending_caption = (
|
|
||||||
el.unique_name,
|
|
||||||
el.text,
|
|
||||||
getattr(el, "with_listing", False),
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
if name == "Heading":
|
|
||||||
_emit_heading(st, el)
|
|
||||||
continue
|
|
||||||
if name == "Paragraph":
|
|
||||||
_emit_paragraph(st, el)
|
|
||||||
continue
|
|
||||||
if name == "Table":
|
|
||||||
cid = ct = None
|
|
||||||
if st.pending_caption:
|
|
||||||
cid, ct, _ = st.pending_caption
|
|
||||||
st.pending_caption = None
|
|
||||||
_emit_table(st, el, cid, ct)
|
|
||||||
continue
|
|
||||||
if name in ("FencedCode", "CodeBlock"):
|
|
||||||
lang = getattr(el, "lang", "") or ""
|
|
||||||
_emit_code(st, el, lang)
|
|
||||||
continue
|
|
||||||
if name == "List":
|
|
||||||
_emit_list(st, el)
|
|
||||||
continue
|
|
||||||
if name == "Equation":
|
|
||||||
_emit_equation(st, el)
|
|
||||||
continue
|
|
||||||
if name == "TOC":
|
|
||||||
continue
|
|
||||||
if name == "Quote":
|
|
||||||
for ch in el.children:
|
|
||||||
if type(ch).__name__ == "Paragraph":
|
|
||||||
st.w(_p("Standard", _inline(ch.children, st)))
|
|
||||||
continue
|
|
||||||
if hasattr(el, "children"):
|
|
||||||
t = _inline(el.children, st).strip()
|
|
||||||
if t:
|
|
||||||
st.w(_p("Standard", t))
|
|
||||||
|
|
||||||
if st.biblio:
|
if st.biblio:
|
||||||
st.w(_h(1, escape_text("СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ")))
|
st.w(_h(1, escape_text("СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ")))
|
||||||
|
|||||||
+3
-8
@@ -2,9 +2,10 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
|
||||||
from xml.sax.saxutils import escape as _xml_escape
|
from xml.sax.saxutils import escape as _xml_escape
|
||||||
|
|
||||||
|
from mdexport.labels import sanitize_label
|
||||||
|
|
||||||
|
|
||||||
def escape_text(s: str) -> str:
|
def escape_text(s: str) -> str:
|
||||||
if not s:
|
if not s:
|
||||||
@@ -12,11 +13,5 @@ def escape_text(s: str) -> str:
|
|||||||
return _xml_escape(s, {"\"": """, "'": "'"})
|
return _xml_escape(s, {"\"": """, "'": "'"})
|
||||||
|
|
||||||
|
|
||||||
_LABEL_RE = re.compile(r"[^\w\-]+", re.UNICODE)
|
|
||||||
|
|
||||||
|
|
||||||
def xml_id(name: str | None, prefix: str = "obj") -> str:
|
def xml_id(name: str | None, prefix: str = "obj") -> str:
|
||||||
if not name:
|
return sanitize_label(name, prefix, "-")
|
||||||
return prefix
|
|
||||||
cleaned = _LABEL_RE.sub("-", name.strip()).strip("-")
|
|
||||||
return f"{prefix}-{cleaned or 'x'}"
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"student": "Тарасов Игорь Алексеевич",
|
"student": "",
|
||||||
"group": "ИНБО-31-23",
|
"group": "",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"author_source": "student",
|
"author_source": "os",
|
||||||
"author": "",
|
"author": "",
|
||||||
"last_modified_by": "",
|
"last_modified_by": "",
|
||||||
"title": "",
|
"title": "",
|
||||||
Binary file not shown.
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
## Установка
|
## Установка
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
poetry install
|
uv sync # окружение по uv.lock (канонический lock-файл)
|
||||||
# или
|
# или
|
||||||
pip install -e .
|
pip install -e .
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ SPECIAL_UNNUMBERED = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
HEADING_RE = re.compile(r"^(#{1,6})\s+(\*?)(.+)$", re.M)
|
HEADING_RE = re.compile(r"^(#{1,6})\s+(\*?)(.+)$", re.M)
|
||||||
|
EMPTY_HEADING_RE = re.compile(r"^(#{1,6})[^\n\S]*\*?[^\n\S]*$", re.M)
|
||||||
FOOTNOTE_RE = re.compile(r"\[\^[^\]]+\]")
|
FOOTNOTE_RE = re.compile(r"\[\^[^\]]+\]")
|
||||||
RIS_RE = re.compile(r"(?i)\bрис\.")
|
RIS_RE = re.compile(r"(?i)\bрис\.")
|
||||||
TABL_RE = re.compile(r"(?i)\bтабл\.")
|
TABL_RE = re.compile(r"(?i)\bтабл\.")
|
||||||
@@ -199,6 +200,19 @@ def check_special_headings(text: str) -> list[Issue]:
|
|||||||
return issues
|
return issues
|
||||||
|
|
||||||
|
|
||||||
|
def check_empty_headings(text: str) -> list[Issue]:
|
||||||
|
"""Заголовок без текста: пронумерованный, но безымянный раздел в содержании."""
|
||||||
|
issues = []
|
||||||
|
for m in EMPTY_HEADING_RE.finditer(text):
|
||||||
|
issues.append(Issue(
|
||||||
|
"heading.empty", "warning",
|
||||||
|
"Заголовок без текста: разделы, подразделы и пункты должны иметь "
|
||||||
|
"заголовки, чётко и кратко отражающие их содержание",
|
||||||
|
_line_of(text, m.start()),
|
||||||
|
))
|
||||||
|
return issues
|
||||||
|
|
||||||
|
|
||||||
def check_forbidden_abbreviations(text: str) -> list[Issue]:
|
def check_forbidden_abbreviations(text: str) -> list[Issue]:
|
||||||
issues = []
|
issues = []
|
||||||
for m in RIS_RE.finditer(text):
|
for m in RIS_RE.finditer(text):
|
||||||
@@ -399,6 +413,8 @@ def check_continuation_hints(
|
|||||||
*,
|
*,
|
||||||
table_continuation: str = "off",
|
table_continuation: str = "off",
|
||||||
listing_continuation: str = "off",
|
listing_continuation: str = "off",
|
||||||
|
syntax_highlighting: bool = False,
|
||||||
|
strict: bool = False,
|
||||||
) -> list[Issue]:
|
) -> list[Issue]:
|
||||||
"""Warn that Word cannot auto-insert «Продолжение…» when mode is off/soft."""
|
"""Warn that Word cannot auto-insert «Продолжение…» when mode is off/soft."""
|
||||||
issues: list[Issue] = []
|
issues: list[Issue] = []
|
||||||
@@ -422,6 +438,13 @@ def check_continuation_hints(
|
|||||||
"Длинный листинг: Word сам не вставит «Продолжение Листинга N». "
|
"Длинный листинг: Word сам не вставит «Продолжение Листинга N». "
|
||||||
"Варианты: --listing-continuation word или caption.",
|
"Варианты: --listing-continuation word или caption.",
|
||||||
))
|
))
|
||||||
|
if syntax_highlighting:
|
||||||
|
issues.append(Issue(
|
||||||
|
"listing.syntax_highlighting",
|
||||||
|
"error" if strict else "warning",
|
||||||
|
"Подсветка синтаксиса задаёт полужирный и цвет в содержимом листинга. "
|
||||||
|
"Табл. 4.1 методички требует «Начертание Обычный», «Цвет текста Авто (чёрный)».",
|
||||||
|
))
|
||||||
return issues
|
return issues
|
||||||
|
|
||||||
|
|
||||||
@@ -490,11 +513,14 @@ def check_markdown(
|
|||||||
*,
|
*,
|
||||||
table_continuation: str = "off",
|
table_continuation: str = "off",
|
||||||
listing_continuation: str = "off",
|
listing_continuation: str = "off",
|
||||||
|
syntax_highlighting: bool = False,
|
||||||
|
strict: bool = False,
|
||||||
) -> list[Issue]:
|
) -> list[Issue]:
|
||||||
profile = get_profile(doc_type)
|
profile = get_profile(doc_type)
|
||||||
issues: list[Issue] = []
|
issues: list[Issue] = []
|
||||||
issues.extend(check_structure(text, profile))
|
issues.extend(check_structure(text, profile))
|
||||||
issues.extend(check_special_headings(text))
|
issues.extend(check_special_headings(text))
|
||||||
|
issues.extend(check_empty_headings(text))
|
||||||
issues.extend(check_pis_headings(text, profile))
|
issues.extend(check_pis_headings(text, profile))
|
||||||
issues.extend(check_forbidden_abbreviations(text))
|
issues.extend(check_forbidden_abbreviations(text))
|
||||||
if profile.require_intro_conclusion:
|
if profile.require_intro_conclusion:
|
||||||
@@ -507,6 +533,8 @@ def check_markdown(
|
|||||||
text,
|
text,
|
||||||
table_continuation=table_continuation,
|
table_continuation=table_continuation,
|
||||||
listing_continuation=listing_continuation,
|
listing_continuation=listing_continuation,
|
||||||
|
syntax_highlighting=syntax_highlighting,
|
||||||
|
strict=strict,
|
||||||
))
|
))
|
||||||
return issues
|
return issues
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ from .renderable.diagram import DiagramFigure
|
|||||||
class Converter:
|
class Converter:
|
||||||
"""Converts markdown file to docx file (MIREA TZ)."""
|
"""Converts markdown file to docx file (MIREA TZ)."""
|
||||||
|
|
||||||
def __init__(self, input_path: str, output_path: str,
|
def __init__(self, input_path: str,
|
||||||
template_path: str = None, debug: bool = False,
|
template_path: str = None, debug: bool = False,
|
||||||
doc_type: str = "practice",
|
doc_type: str = "practice",
|
||||||
heading_numbering: str = DEFAULT_HEADING_NUMBERING,
|
heading_numbering: str = DEFAULT_HEADING_NUMBERING,
|
||||||
@@ -67,7 +67,6 @@ class Converter:
|
|||||||
f"listing_continuation must be one of {LISTING_CONTINUATION_MODES}, "
|
f"listing_continuation must be one of {LISTING_CONTINUATION_MODES}, "
|
||||||
f"got {listing_continuation!r}"
|
f"got {listing_continuation!r}"
|
||||||
)
|
)
|
||||||
self._output_path = output_path
|
|
||||||
self._doc_type = doc_type
|
self._doc_type = doc_type
|
||||||
self._heading_numbering = heading_numbering
|
self._heading_numbering = heading_numbering
|
||||||
self._emdash_to_hyphen = emdash_to_hyphen
|
self._emdash_to_hyphen = emdash_to_hyphen
|
||||||
|
|||||||
+45
-3
@@ -400,6 +400,43 @@ def _render_dot_local(dot: str, out_path: Path, fmt: str, *, context: bool) -> b
|
|||||||
return out_path.is_file() and out_path.stat().st_size > 0
|
return out_path.is_file() and out_path.stat().st_size > 0
|
||||||
|
|
||||||
|
|
||||||
|
def _render_dot_local_combined(
|
||||||
|
dot: str,
|
||||||
|
targets: list[tuple[Path, str]],
|
||||||
|
*,
|
||||||
|
context: bool,
|
||||||
|
) -> set[str]:
|
||||||
|
"""
|
||||||
|
One Graphviz process for several formats (`-Tpng -o… -Tsvg -o…`).
|
||||||
|
Returns the formats actually written; the caller retries the rest one by one.
|
||||||
|
"""
|
||||||
|
if len(targets) < 2:
|
||||||
|
return set()
|
||||||
|
engine = resolve_graphviz_engine(context=context)
|
||||||
|
if not engine:
|
||||||
|
return set()
|
||||||
|
cmd = [engine]
|
||||||
|
for out_path, fmt in targets:
|
||||||
|
cmd += [f"-T{fmt}", f"-o{out_path}"]
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
cmd,
|
||||||
|
input=_readable_dot(dot),
|
||||||
|
encoding="utf-8",
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
timeout=180,
|
||||||
|
env=_graphviz_run_env(engine),
|
||||||
|
)
|
||||||
|
except (OSError, subprocess.CalledProcessError, subprocess.TimeoutExpired) as exc:
|
||||||
|
_log.warning("Graphviz %s (multi-format) failed: %s", engine, exc)
|
||||||
|
return set()
|
||||||
|
return {
|
||||||
|
fmt for out_path, fmt in targets
|
||||||
|
if Path(out_path).is_file() and Path(out_path).stat().st_size > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _render_dot_kroki(dot: str, out_path: Path, fmt: str) -> bool:
|
def _render_dot_kroki(dot: str, out_path: Path, fmt: str) -> bool:
|
||||||
try:
|
try:
|
||||||
from .diagram_renderer import (
|
from .diagram_renderer import (
|
||||||
@@ -455,8 +492,14 @@ def write_dfd_outputs(
|
|||||||
dot, is_context = _compile_dot(source)
|
dot, is_context = _compile_dot(source)
|
||||||
out_png = Path(out_png)
|
out_png = Path(out_png)
|
||||||
out_png.parent.mkdir(parents=True, exist_ok=True)
|
out_png.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
out_svg = Path(out_svg) if out_svg is not None else None
|
||||||
|
|
||||||
ok_png = _render_dot_local(dot, out_png, "png", context=is_context)
|
targets: list[tuple[Path, str]] = [(out_png, "png")]
|
||||||
|
if out_svg is not None:
|
||||||
|
targets.append((out_svg, "svg"))
|
||||||
|
done = _render_dot_local_combined(dot, targets, context=is_context)
|
||||||
|
|
||||||
|
ok_png = "png" in done or _render_dot_local(dot, out_png, "png", context=is_context)
|
||||||
if not ok_png:
|
if not ok_png:
|
||||||
ok_png = _render_dot_kroki(dot, out_png, "png")
|
ok_png = _render_dot_kroki(dot, out_png, "png")
|
||||||
if not ok_png:
|
if not ok_png:
|
||||||
@@ -467,8 +510,7 @@ def write_dfd_outputs(
|
|||||||
"DSL: https://github.com/pbauermeister/dfd"
|
"DSL: https://github.com/pbauermeister/dfd"
|
||||||
)
|
)
|
||||||
|
|
||||||
if out_svg is not None:
|
if out_svg is not None and "svg" not in done:
|
||||||
out_svg = Path(out_svg)
|
|
||||||
ok_svg = _render_dot_local(dot, out_svg, "svg", context=is_context)
|
ok_svg = _render_dot_local(dot, out_svg, "svg", context=is_context)
|
||||||
if not ok_svg:
|
if not ok_svg:
|
||||||
_render_dot_kroki(dot, out_svg, "svg")
|
_render_dot_kroki(dot, out_svg, "svg")
|
||||||
|
|||||||
+208
-30
@@ -60,6 +60,11 @@ PLANTUML_JAR_URL = (
|
|||||||
f"https://github.com/plantuml/plantuml/releases/download/"
|
f"https://github.com/plantuml/plantuml/releases/download/"
|
||||||
f"v{PLANTUML_VERSION}/plantuml-{PLANTUML_VERSION}.jar"
|
f"v{PLANTUML_VERSION}/plantuml-{PLANTUML_VERSION}.jar"
|
||||||
)
|
)
|
||||||
|
# Part of the cache key: a new jar draws the same source differently.
|
||||||
|
PLANTUML_CACHE_PREFIX = f"plantuml-{PLANTUML_VERSION}\n"
|
||||||
|
|
||||||
|
# One JVM per chunk; keeps the command line far below the Windows 32k limit.
|
||||||
|
PLANTUML_BATCH_CHUNK = 60
|
||||||
|
|
||||||
_log = logging.getLogger(__name__)
|
_log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -99,6 +104,7 @@ def configure_diagrams(
|
|||||||
global _CONFIG
|
global _CONFIG
|
||||||
ensure_user_schemes()
|
ensure_user_schemes()
|
||||||
configure_schemes(schemes_path=schemes_path, md_dir=md_dir)
|
configure_schemes(schemes_path=schemes_path, md_dir=md_dir)
|
||||||
|
reset_diagram_batch()
|
||||||
try:
|
try:
|
||||||
from .mermaid_renderer import ensure_playwright_browsers_env, reset_mermaid_session
|
from .mermaid_renderer import ensure_playwright_browsers_env, reset_mermaid_session
|
||||||
|
|
||||||
@@ -298,17 +304,45 @@ def _is_svg(data: bytes) -> bool:
|
|||||||
return head.startswith(b"<svg") or head.startswith(b"<?xml") or b"<svg" in head
|
return head.startswith(b"<svg") or head.startswith(b"<?xml") or b"<svg" in head
|
||||||
|
|
||||||
|
|
||||||
def _render_plantuml_jar_one(
|
@dataclass
|
||||||
source: str,
|
class _PumlJob:
|
||||||
out_path: Path,
|
"""One PlantUML source and where its rendered file must land."""
|
||||||
|
|
||||||
|
source: str
|
||||||
|
out_path: Path
|
||||||
|
|
||||||
|
|
||||||
|
def _pick_produced(out_dir: Path, fmt: str) -> Path | None:
|
||||||
|
"""Output name follows `@startuml <name>`, so take what the jar actually wrote."""
|
||||||
|
if not out_dir.is_dir():
|
||||||
|
return None
|
||||||
|
files = [
|
||||||
|
p for p in out_dir.iterdir()
|
||||||
|
if p.is_file() and p.suffix.lower() == f".{fmt}"
|
||||||
|
]
|
||||||
|
if not files:
|
||||||
|
return None
|
||||||
|
# `newpage` adds name_001, name_002…; the shortest name is the first page.
|
||||||
|
files.sort(key=lambda p: (len(p.name), p.name))
|
||||||
|
return files[0]
|
||||||
|
|
||||||
|
|
||||||
|
def _plantuml_run_chunk(
|
||||||
|
chunk: list[_PumlJob],
|
||||||
jar: str,
|
jar: str,
|
||||||
fmt: Literal["png", "svg"],
|
fmt: Literal["png", "svg"],
|
||||||
) -> bool:
|
done: list[bool],
|
||||||
if not jar or not os.path.isfile(jar) or not _java_available():
|
offset: int,
|
||||||
return False
|
) -> None:
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory(prefix="md2gost-puml-") as tmp:
|
||||||
src = Path(tmp) / "diagram.puml"
|
root = Path(tmp)
|
||||||
src.write_text(source, encoding="utf-8")
|
sources: list[str] = []
|
||||||
|
for i, job in enumerate(chunk):
|
||||||
|
sub = root / f"{i:04d}"
|
||||||
|
sub.mkdir()
|
||||||
|
src = sub / "src.puml"
|
||||||
|
src.write_text(job.source, encoding="utf-8")
|
||||||
|
sources.append(str(src))
|
||||||
cmd = [
|
cmd = [
|
||||||
"java",
|
"java",
|
||||||
f"-Dplantuml.include.path={diagrams_dir().resolve()}",
|
f"-Dplantuml.include.path={diagrams_dir().resolve()}",
|
||||||
@@ -316,27 +350,63 @@ def _render_plantuml_jar_one(
|
|||||||
"-jar", jar,
|
"-jar", jar,
|
||||||
f"-t{fmt}",
|
f"-t{fmt}",
|
||||||
"-charset", "UTF-8",
|
"-charset", "UTF-8",
|
||||||
"-o", tmp,
|
# Relative -o resolves against each input's own folder, so two sources
|
||||||
str(src),
|
# naming themselves `@startuml Name` cannot collide on one output file.
|
||||||
|
"-o", "out",
|
||||||
|
*sources,
|
||||||
]
|
]
|
||||||
|
timeout = min(600, 120 + 30 * (len(chunk) - 1))
|
||||||
try:
|
try:
|
||||||
proc = subprocess.run(
|
proc = subprocess.run(
|
||||||
cmd, capture_output=True, text=True, timeout=120, check=False,
|
cmd, capture_output=True, text=True, timeout=timeout, check=False,
|
||||||
)
|
)
|
||||||
except (OSError, subprocess.TimeoutExpired) as e:
|
except (OSError, subprocess.TimeoutExpired) as e:
|
||||||
_log.warning("PlantUML jar failed: %s", e)
|
_log.warning("PlantUML jar failed: %s", e)
|
||||||
return False
|
return
|
||||||
produced = Path(tmp) / f"diagram.{fmt}"
|
if proc.returncode != 0:
|
||||||
if proc.returncode != 0 or not produced.is_file():
|
|
||||||
_log.warning("PlantUML jar error: %s", proc.stderr or proc.stdout)
|
_log.warning("PlantUML jar error: %s", proc.stderr or proc.stdout)
|
||||||
return False
|
return
|
||||||
data = produced.read_bytes()
|
for i, job in enumerate(chunk):
|
||||||
if fmt == "png" and not _is_png(data):
|
produced = _pick_produced(root / f"{i:04d}" / "out", fmt)
|
||||||
return False
|
if produced is None:
|
||||||
if fmt == "svg" and not _is_svg(data):
|
_log.warning("PlantUML jar: %s не создан", fmt)
|
||||||
return False
|
continue
|
||||||
shutil.copyfile(produced, out_path)
|
data = produced.read_bytes()
|
||||||
return True
|
if fmt == "png" and not _is_png(data):
|
||||||
|
continue
|
||||||
|
if fmt == "svg" and not _is_svg(data):
|
||||||
|
continue
|
||||||
|
job.out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copyfile(produced, job.out_path)
|
||||||
|
done[offset + i] = True
|
||||||
|
|
||||||
|
|
||||||
|
def _plantuml_run(
|
||||||
|
jobs: list[_PumlJob],
|
||||||
|
jar: str,
|
||||||
|
fmt: Literal["png", "svg"],
|
||||||
|
) -> list[bool]:
|
||||||
|
"""Render every job in one JVM per chunk. Returns per-job success flags."""
|
||||||
|
if not jobs:
|
||||||
|
return []
|
||||||
|
if not jar or not os.path.isfile(jar) or not _java_available():
|
||||||
|
return [False] * len(jobs)
|
||||||
|
done = [False] * len(jobs)
|
||||||
|
for start in range(0, len(jobs), PLANTUML_BATCH_CHUNK):
|
||||||
|
_plantuml_run_chunk(
|
||||||
|
jobs[start:start + PLANTUML_BATCH_CHUNK], jar, fmt, done, start,
|
||||||
|
)
|
||||||
|
return done
|
||||||
|
|
||||||
|
|
||||||
|
def _render_plantuml_jar_one(
|
||||||
|
source: str,
|
||||||
|
out_path: Path,
|
||||||
|
jar: str,
|
||||||
|
fmt: Literal["png", "svg"],
|
||||||
|
) -> bool:
|
||||||
|
done = _plantuml_run([_PumlJob(source, Path(out_path))], jar, fmt)
|
||||||
|
return bool(done and done[0])
|
||||||
|
|
||||||
|
|
||||||
def _render_plantuml_jar(
|
def _render_plantuml_jar(
|
||||||
@@ -355,6 +425,100 @@ def _render_plantuml_jar(
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _plantuml_scaled(prepared: str, scale: float) -> tuple[str, float]:
|
||||||
|
"""Return (source, pixel_scale); pixel_scale stays 1 when the author set scale."""
|
||||||
|
if scale <= 1:
|
||||||
|
return prepared, 1.0
|
||||||
|
boosted = _ensure_plantuml_png_scale(prepared, scale)
|
||||||
|
return boosted, (scale if boosted != prepared else 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
# Fences seen while parsing, so the whole document can share one JVM.
|
||||||
|
_PENDING: list[tuple[str, str]] = []
|
||||||
|
_PENDING_SEEN: set[tuple[str, str]] = set()
|
||||||
|
_BATCH_FLUSHED = False
|
||||||
|
|
||||||
|
|
||||||
|
def register_pending_diagram(lang: str, source: str) -> None:
|
||||||
|
"""Announce a diagram fence found during parsing (see _flush_plantuml_batch)."""
|
||||||
|
key = (lang or "", source or "")
|
||||||
|
if key in _PENDING_SEEN:
|
||||||
|
return
|
||||||
|
_PENDING_SEEN.add(key)
|
||||||
|
_PENDING.append(key)
|
||||||
|
|
||||||
|
|
||||||
|
def reset_diagram_batch() -> None:
|
||||||
|
global _BATCH_FLUSHED
|
||||||
|
_PENDING.clear()
|
||||||
|
_PENDING_SEEN.clear()
|
||||||
|
_BATCH_FLUSHED = False
|
||||||
|
|
||||||
|
|
||||||
|
def _flush_plantuml_batch() -> None:
|
||||||
|
"""
|
||||||
|
Pre-pass before the first diagram renders: draw every cache-missing PlantUML
|
||||||
|
source of the document in one JVM instead of one (two in svg mode) per diagram.
|
||||||
|
Anything that fails here simply stays a cache miss and takes the normal path.
|
||||||
|
"""
|
||||||
|
global _BATCH_FLUSHED
|
||||||
|
if _BATCH_FLUSHED:
|
||||||
|
return
|
||||||
|
_BATCH_FLUSHED = True
|
||||||
|
pending = list(_PENDING)
|
||||||
|
_PENDING.clear()
|
||||||
|
_PENDING_SEEN.clear()
|
||||||
|
if len(pending) < 2:
|
||||||
|
return
|
||||||
|
jar = resolve_plantuml_jar(_CONFIG.plantuml_jar or None, download=False)
|
||||||
|
if not jar or not _java_available():
|
||||||
|
return
|
||||||
|
|
||||||
|
want_svg = _CONFIG.diagram_format == "svg"
|
||||||
|
scale = _CONFIG.diagram_scale
|
||||||
|
png_jobs: list[_PumlJob] = []
|
||||||
|
svg_jobs: list[_PumlJob] = []
|
||||||
|
seen: set[str] = set()
|
||||||
|
for lang, source in pending:
|
||||||
|
try:
|
||||||
|
prepared, kroki_type = prepare_source(lang, source)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if kroki_type != "plantuml":
|
||||||
|
continue
|
||||||
|
prepared, _ = _plantuml_scaled(prepared, scale)
|
||||||
|
out_png, out_svg = _cache_paths(prepared, cache_prefix=PLANTUML_CACHE_PREFIX)
|
||||||
|
if str(out_png) in seen:
|
||||||
|
continue
|
||||||
|
seen.add(str(out_png))
|
||||||
|
if not (out_png.is_file() and out_png.stat().st_size > 0):
|
||||||
|
png_jobs.append(_PumlJob(prepared, out_png))
|
||||||
|
if want_svg and not (out_svg.is_file() and out_svg.stat().st_size > 0):
|
||||||
|
svg_jobs.append(_PumlJob(prepared, out_svg))
|
||||||
|
|
||||||
|
if not png_jobs and not svg_jobs:
|
||||||
|
return
|
||||||
|
_log.info(
|
||||||
|
"PlantUML: пакетный рендер %d диаграмм одной JVM",
|
||||||
|
max(len(png_jobs), len(svg_jobs)),
|
||||||
|
)
|
||||||
|
if png_jobs:
|
||||||
|
_plantuml_run(png_jobs, jar, "png")
|
||||||
|
if svg_jobs:
|
||||||
|
_plantuml_run(svg_jobs, jar, "svg")
|
||||||
|
|
||||||
|
|
||||||
|
def shutdown_diagrams() -> None:
|
||||||
|
"""Release per-conversion resources: headless browser and the batch queue."""
|
||||||
|
reset_diagram_batch()
|
||||||
|
try:
|
||||||
|
from .mermaid_renderer import reset_mermaid_session
|
||||||
|
|
||||||
|
reset_mermaid_session()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _render_kroki(
|
def _render_kroki(
|
||||||
source: str,
|
source: str,
|
||||||
diagram_type: str,
|
diagram_type: str,
|
||||||
@@ -414,22 +578,32 @@ def render_diagram(
|
|||||||
diagram_scale: float | None = None,
|
diagram_scale: float | None = None,
|
||||||
) -> DiagramRenderResult:
|
) -> DiagramRenderResult:
|
||||||
"""Render diagram to PNG (+ optional SVG). Raises RuntimeError if all backends fail."""
|
"""Render diagram to PNG (+ optional SVG). Raises RuntimeError if all backends fail."""
|
||||||
ensure_user_schemes()
|
# Batch only the plain document path; explicit overrides would not match _CONFIG.
|
||||||
|
if not any(
|
||||||
|
arg is not None
|
||||||
|
for arg in (
|
||||||
|
plantuml_jar, kroki_url, fallback, cache_dir, diagram_format, diagram_scale,
|
||||||
|
)
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
_flush_plantuml_batch()
|
||||||
|
except Exception as exc:
|
||||||
|
_log.warning("PlantUML: пакетный рендер не удался (%s)", exc)
|
||||||
|
|
||||||
prepared, kroki_type = prepare_source(lang, source)
|
prepared, kroki_type = prepare_source(lang, source)
|
||||||
scale = _CONFIG.diagram_scale if diagram_scale is None else float(diagram_scale)
|
scale = _CONFIG.diagram_scale if diagram_scale is None else float(diagram_scale)
|
||||||
if scale < 1:
|
if scale < 1:
|
||||||
scale = 1.0
|
scale = 1.0
|
||||||
# pixel_scale: only when we injected scale (author override → treat as 1 for display).
|
# pixel_scale: only when we injected scale (author override → treat as 1 for display).
|
||||||
pixel_scale = 1.0
|
pixel_scale = 1.0
|
||||||
if kroki_type == "plantuml" and scale > 1:
|
if kroki_type == "plantuml":
|
||||||
before = prepared
|
prepared, pixel_scale = _plantuml_scaled(prepared, scale)
|
||||||
prepared = _ensure_plantuml_png_scale(prepared, scale)
|
|
||||||
if prepared != before:
|
|
||||||
pixel_scale = scale
|
|
||||||
# Mermaid: pixel_scale set only after successful local hi-res render (not Kroki).
|
# Mermaid: pixel_scale set only after successful local hi-res render (not Kroki).
|
||||||
|
|
||||||
cache_prefix = ""
|
cache_prefix = ""
|
||||||
if kroki_type == "mermaid":
|
if kroki_type == "plantuml":
|
||||||
|
cache_prefix = PLANTUML_CACHE_PREFIX
|
||||||
|
elif kroki_type == "mermaid":
|
||||||
try:
|
try:
|
||||||
from .mermaid_renderer import CACHE_KEY_PREFIX
|
from .mermaid_renderer import CACHE_KEY_PREFIX
|
||||||
|
|
||||||
@@ -633,8 +807,12 @@ __all__ = [
|
|||||||
"fetch_plantuml_jar",
|
"fetch_plantuml_jar",
|
||||||
"is_diagram_lang",
|
"is_diagram_lang",
|
||||||
"prepare_source",
|
"prepare_source",
|
||||||
|
"register_pending_diagram",
|
||||||
"render_diagram",
|
"render_diagram",
|
||||||
|
"reset_diagram_batch",
|
||||||
"resolve_plantuml_jar",
|
"resolve_plantuml_jar",
|
||||||
|
"shutdown_diagrams",
|
||||||
|
"PLANTUML_CACHE_PREFIX",
|
||||||
"PLANTUML_JAR_URL",
|
"PLANTUML_JAR_URL",
|
||||||
"PLANTUML_VERSION",
|
"PLANTUML_VERSION",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class Heading(BlockElement):
|
|||||||
def __init__(self, match: Match[str]) -> None:
|
def __init__(self, match: Match[str]) -> None:
|
||||||
self.level = len(match.group(1))
|
self.level = len(match.group(1))
|
||||||
inline_body = match.group(2).strip()
|
inline_body = match.group(2).strip()
|
||||||
self.numbered = not (inline_body[0] == "*")
|
self.numbered = not inline_body.startswith("*")
|
||||||
if not self.numbered:
|
if not self.numbered:
|
||||||
inline_body = inline_body[1:]
|
inline_body = inline_body[1:]
|
||||||
self.inline_body = inline_body
|
self.inline_body = inline_body
|
||||||
|
|||||||
@@ -2018,32 +2018,6 @@ def _draw_arrow(
|
|||||||
return paths
|
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(
|
def render_idef0(
|
||||||
diagram: Idef0Diagram,
|
diagram: Idef0Diagram,
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -102,9 +102,6 @@ def assign_numbers(renderables: list[Renderable],
|
|||||||
r.set_listing_number(lnum)
|
r.set_listing_number(lnum)
|
||||||
if label:
|
if label:
|
||||||
registry.put("Листинг", label, lnum)
|
registry.put("Листинг", label, lnum)
|
||||||
# Images nested in paragraphs
|
|
||||||
if isinstance(r, Paragraph):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Images attached to paragraphs
|
# Images attached to paragraphs
|
||||||
if isinstance(r, Paragraph):
|
if isinstance(r, Paragraph):
|
||||||
|
|||||||
+13
-4
@@ -8,15 +8,24 @@ from lxml.etree import _Element
|
|||||||
|
|
||||||
from . import package_dir
|
from . import package_dir
|
||||||
|
|
||||||
|
_MML2OMML_TRANSFORM: etree.XSLT | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def _mml2omml_transform() -> etree.XSLT:
|
||||||
|
"""The stylesheet is stateless and reusable, so parse and compile it once."""
|
||||||
|
global _MML2OMML_TRANSFORM
|
||||||
|
if _MML2OMML_TRANSFORM is None:
|
||||||
|
_MML2OMML_TRANSFORM = etree.XSLT(etree.parse(
|
||||||
|
os.path.join(package_dir(), "mml2omml")
|
||||||
|
))
|
||||||
|
return _MML2OMML_TRANSFORM
|
||||||
|
|
||||||
|
|
||||||
def latex_to_omml(latex_equation: str) -> _Element:
|
def latex_to_omml(latex_equation: str) -> _Element:
|
||||||
try:
|
try:
|
||||||
mathml = latex2mathml.converter.convert(latex_equation)
|
mathml = latex2mathml.converter.convert(latex_equation)
|
||||||
tree = etree.fromstring(mathml)
|
tree = etree.fromstring(mathml)
|
||||||
xslt = etree.parse(
|
transform = _mml2omml_transform()
|
||||||
os.path.join(package_dir(), "mml2omml")
|
|
||||||
)
|
|
||||||
transform = etree.XSLT(xslt)
|
|
||||||
new_dom = transform(tree)
|
new_dom = transform(tree)
|
||||||
word_math = new_dom.getroot()
|
word_math = new_dom.getroot()
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -404,13 +404,6 @@ class _BrowserSession:
|
|||||||
self._page.wait_for_function("typeof window.__md2gostRender === 'function'")
|
self._page.wait_for_function("typeof window.__md2gostRender === 'function'")
|
||||||
self._scale = want
|
self._scale = want
|
||||||
|
|
||||||
def render_svg(self, source: str) -> str:
|
|
||||||
prepared = prepare_mermaid_source(source, html_labels=True)
|
|
||||||
svg = self._page.evaluate("(code) => window.__md2gostRender(code)", prepared)
|
|
||||||
if not isinstance(svg, str) or "<svg" not in svg.lower():
|
|
||||||
raise RuntimeError("browser mermaid.render returned no SVG")
|
|
||||||
return svg
|
|
||||||
|
|
||||||
def render_png(self, source: str, *, scale: float = 1.0) -> tuple[str, bytes]:
|
def render_png(self, source: str, *, scale: float = 1.0) -> tuple[str, bytes]:
|
||||||
"""
|
"""
|
||||||
Browser: htmlLabels + screenshot at viewBox size (layout + Cyrillic + hi-res).
|
Browser: htmlLabels + screenshot at viewBox size (layout + Cyrillic + hi-res).
|
||||||
|
|||||||
+10
-1
@@ -294,6 +294,8 @@ def convert(req: ConvertRequest, log: LogFn | None = None) -> ConvertResult:
|
|||||||
req.doc_type,
|
req.doc_type,
|
||||||
table_continuation=req.table_continuation,
|
table_continuation=req.table_continuation,
|
||||||
listing_continuation=req.listing_continuation,
|
listing_continuation=req.listing_continuation,
|
||||||
|
syntax_highlighting=req.syntax_highlighting,
|
||||||
|
strict=req.strict,
|
||||||
)
|
)
|
||||||
check_report = format_report(issues)
|
check_report = format_report(issues)
|
||||||
emit(check_report)
|
emit(check_report)
|
||||||
@@ -319,7 +321,7 @@ def convert(req: ConvertRequest, log: LogFn | None = None) -> ConvertResult:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
converter = Converter(
|
converter = Converter(
|
||||||
filename, output, template, req.debug,
|
filename, template, req.debug,
|
||||||
doc_type=req.doc_type,
|
doc_type=req.doc_type,
|
||||||
heading_numbering=req.heading_numbering,
|
heading_numbering=req.heading_numbering,
|
||||||
emdash_to_hyphen=req.emdash_to_hyphen,
|
emdash_to_hyphen=req.emdash_to_hyphen,
|
||||||
@@ -404,6 +406,13 @@ def convert(req: ConvertRequest, log: LogFn | None = None) -> ConvertResult:
|
|||||||
message=f"Ошибка конвертации: {exc}",
|
message=f"Ошибка конвертации: {exc}",
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
|
# Headless Chrome would otherwise stay alive until the next conversion.
|
||||||
|
try:
|
||||||
|
from .diagram_renderer import shutdown_diagrams
|
||||||
|
|
||||||
|
shutdown_diagrams()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
root_log.removeHandler(handler)
|
root_log.removeHandler(handler)
|
||||||
attached_diag.removeHandler(handler)
|
attached_diag.removeHandler(handler)
|
||||||
attached_diag.setLevel(prev_diag_level)
|
attached_diag.setLevel(prev_diag_level)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
|
|||||||
from md2gost.layout_tracker import LayoutState
|
from md2gost.layout_tracker import LayoutState
|
||||||
from md2gost.renderable import Renderable
|
from md2gost.renderable import Renderable
|
||||||
from md2gost.rendered_info import RenderedInfo
|
from md2gost.rendered_info import RenderedInfo
|
||||||
from .paragraph_sizer import ParagraphSizer
|
from .paragraph_sizer import ParagraphSizerResult, measure_paragraph
|
||||||
from ..util import create_element
|
from ..util import create_element
|
||||||
|
|
||||||
|
|
||||||
@@ -35,8 +35,16 @@ class Caption(Renderable):
|
|||||||
self._parent = parent
|
self._parent = parent
|
||||||
self._before = before
|
self._before = before
|
||||||
self._category = category
|
self._category = category
|
||||||
|
self._size_cache: dict = {}
|
||||||
self._docx_paragraph = DocxParagraph(create_element("w:p"), parent)
|
self._docx_paragraph = DocxParagraph(create_element("w:p"), parent)
|
||||||
|
|
||||||
|
# Table/listing captions use keep_with_next. pageBreakBefore + keepWithNext
|
||||||
|
# on a paragraph that Word already wraps to a new page produces a blank
|
||||||
|
# page that the layout tracker / debug overlay never sees. Written here
|
||||||
|
# rather than at the end of render() so that measuring the caption twice
|
||||||
|
# (fit check, then emit) sees the same paragraph.
|
||||||
|
self._docx_paragraph.paragraph_format.page_break_before = False
|
||||||
|
|
||||||
style_name = CAPTION_STYLES.get(category, "Caption")
|
style_name = CAPTION_STYLES.get(category, "Caption")
|
||||||
try:
|
try:
|
||||||
self._docx_paragraph.style = style_name
|
self._docx_paragraph.style = style_name
|
||||||
@@ -58,18 +66,20 @@ class Caption(Renderable):
|
|||||||
def center(self):
|
def center(self):
|
||||||
self._docx_paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
|
self._docx_paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
|
||||||
|
|
||||||
|
def measure(self, previous_paragraph: DocxParagraph | None, max_width) -> ParagraphSizerResult:
|
||||||
|
"""Line count and spacing of the caption line against a given predecessor."""
|
||||||
|
return measure_paragraph(
|
||||||
|
self._size_cache, self._docx_paragraph, previous_paragraph, max_width
|
||||||
|
)
|
||||||
|
|
||||||
def render(self, previous_rendered: RenderedInfo, layout_state: LayoutState) -> Generator[
|
def render(self, previous_rendered: RenderedInfo, layout_state: LayoutState) -> Generator[
|
||||||
"RenderedInfo | Renderable", None, None]:
|
"RenderedInfo | Renderable", None, None]:
|
||||||
height_data = ParagraphSizer(
|
height_data = self.measure(
|
||||||
self._docx_paragraph,
|
|
||||||
previous_rendered.docx_element
|
previous_rendered.docx_element
|
||||||
if previous_rendered and isinstance(previous_rendered.docx_element, DocxParagraph) else None,
|
if previous_rendered and isinstance(previous_rendered.docx_element, DocxParagraph) else None,
|
||||||
layout_state.max_width
|
layout_state.max_width
|
||||||
).calculate_height()
|
)
|
||||||
|
|
||||||
# Table/listing captions use keep_with_next. pageBreakBefore + keepWithNext
|
|
||||||
# on a paragraph that Word already wraps to a new page produces a blank
|
|
||||||
# page that the layout tracker / debug overlay never sees.
|
|
||||||
leftover = 0
|
leftover = 0
|
||||||
need_new_page = (
|
need_new_page = (
|
||||||
self._before
|
self._before
|
||||||
@@ -79,11 +89,6 @@ class Caption(Renderable):
|
|||||||
)
|
)
|
||||||
if need_new_page:
|
if need_new_page:
|
||||||
leftover = layout_state.remaining_page_height
|
leftover = layout_state.remaining_page_height
|
||||||
height_data = ParagraphSizer(
|
height_data = self.measure(None, layout_state.max_width)
|
||||||
self._docx_paragraph,
|
|
||||||
None,
|
|
||||||
layout_state.max_width
|
|
||||||
).calculate_height()
|
|
||||||
|
|
||||||
self._docx_paragraph.paragraph_format.page_break_before = False
|
|
||||||
yield RenderedInfo(self._docx_paragraph, height_data.full + leftover)
|
yield RenderedInfo(self._docx_paragraph, height_data.full + leftover)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from .image import Image
|
|||||||
from .listing import Listing
|
from .listing import Listing
|
||||||
from .renderable import Renderable
|
from .renderable import Renderable
|
||||||
from .requires_numbering import RequiresNumbering
|
from .requires_numbering import RequiresNumbering
|
||||||
from ..diagram_renderer import render_diagram
|
from ..diagram_renderer import register_pending_diagram, render_diagram
|
||||||
from ..layout_tracker import LayoutState
|
from ..layout_tracker import LayoutState
|
||||||
from ..rendered_info import RenderedInfo
|
from ..rendered_info import RenderedInfo
|
||||||
|
|
||||||
@@ -34,6 +34,9 @@ class DiagramFigure(Renderable, RequiresNumbering):
|
|||||||
)
|
)
|
||||||
self._number = None
|
self._number = None
|
||||||
self._listing_number = None
|
self._listing_number = None
|
||||||
|
# Parsing runs fully before rendering, so this collects the whole document
|
||||||
|
# and lets the first render() draw every PlantUML source in one JVM.
|
||||||
|
register_pending_diagram(lang, source)
|
||||||
self.landscape = bool(caption_info and caption_info.landscape)
|
self.landscape = bool(caption_info and caption_info.landscape)
|
||||||
if caption_info and caption_info.unique_name:
|
if caption_info and caption_info.unique_name:
|
||||||
self.unique_name = caption_info.unique_name
|
self.unique_name = caption_info.unique_name
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ class Image(Renderable, RequiresNumbering):
|
|||||||
logging.warning("SVG blip attach failed (%s): %s", svg_path, exc)
|
logging.warning("SVG blip attach failed (%s): %s", svg_path, exc)
|
||||||
|
|
||||||
self._number = None
|
self._number = None
|
||||||
|
self._caption: Caption | None = None
|
||||||
self.landscape = bool(caption_info and caption_info.landscape)
|
self.landscape = bool(caption_info and caption_info.landscape)
|
||||||
if caption_info and caption_info.unique_name:
|
if caption_info and caption_info.unique_name:
|
||||||
self.unique_name = caption_info.unique_name
|
self.unique_name = caption_info.unique_name
|
||||||
@@ -89,6 +90,21 @@ class Image(Renderable, RequiresNumbering):
|
|||||||
|
|
||||||
def set_number(self, number: str):
|
def set_number(self, number: str):
|
||||||
self._number = number
|
self._number = number
|
||||||
|
self._caption = None
|
||||||
|
|
||||||
|
def _get_caption(self) -> Caption:
|
||||||
|
"""One caption object per figure.
|
||||||
|
|
||||||
|
Paragraph.render() lays a figure out once to learn its height and the
|
||||||
|
Renderer then renders it for real; a shared caption keeps the second
|
||||||
|
pass from re-measuring the same line.
|
||||||
|
"""
|
||||||
|
if self._caption is None:
|
||||||
|
self._caption = Caption(
|
||||||
|
self._parent, "Рисунок", self._caption_info, self._number, False
|
||||||
|
)
|
||||||
|
self._caption.center()
|
||||||
|
return self._caption
|
||||||
|
|
||||||
def _reset_native_size(self) -> None:
|
def _reset_native_size(self) -> None:
|
||||||
if self._image is None or self._native_width is None or self._native_height is None:
|
if self._image is None or self._native_width is None or self._native_height is None:
|
||||||
@@ -142,7 +158,4 @@ class Image(Renderable, RequiresNumbering):
|
|||||||
|
|
||||||
layout_state.add_height(rendered_image.height)
|
layout_state.add_height(rendered_image.height)
|
||||||
|
|
||||||
caption = Caption(self._parent, "Рисунок", self._caption_info, self._number, False)
|
yield from self._get_caption().render(rendered_image, copy(layout_state))
|
||||||
caption.center()
|
|
||||||
|
|
||||||
yield from caption.render(rendered_image, copy(layout_state))
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class DocxParagraphPygmentsFormatter(Formatter):
|
|||||||
self._styles[token] = style
|
self._styles[token] = style
|
||||||
|
|
||||||
def _add_run_to_last_paragraph(self, text, style):
|
def _add_run_to_last_paragraph(self, text, style):
|
||||||
self._paragraphs[-1].add_run(text, style["bold"] or None, style["italic"] in style or None,
|
self._paragraphs[-1].add_run(text, style["bold"] or None, style.get("italic") or None,
|
||||||
RGBColor.from_string(style['color']) if style['color'] else None)
|
RGBColor.from_string(style['color']) if style['color'] else None)
|
||||||
|
|
||||||
def format(self, tokensource, outfile):
|
def format(self, tokensource, outfile):
|
||||||
@@ -63,20 +63,6 @@ class Listing(Renderable, RequiresNumbering):
|
|||||||
if caption_info and caption_info.unique_name:
|
if caption_info and caption_info.unique_name:
|
||||||
self.unique_name = caption_info.unique_name
|
self.unique_name = caption_info.unique_name
|
||||||
|
|
||||||
def _create_table(self, parent, width: Length):
|
|
||||||
# Kept for tests / callers that still expect the helper; render uses multi-row.
|
|
||||||
left_margin = Twips(int(
|
|
||||||
parent.part.styles["Normal Table"]._element.xpath("w:tblPr/w:tblCellMar/w:left")[0].attrib[
|
|
||||||
"{http://schemas.openxmlformats.org/wordprocessingml/2006/main}w"]))
|
|
||||||
right_margin = Twips(int(
|
|
||||||
parent.part.styles["Normal Table"]._element.xpath("w:tblPr/w:tblCellMar/w:right")[0].attrib[
|
|
||||||
"{http://schemas.openxmlformats.org/wordprocessingml/2006/main}w"]))
|
|
||||||
|
|
||||||
return create_table(
|
|
||||||
parent, 1, 1,
|
|
||||||
Twips(_twips(width) + _twips(left_margin) + _twips(right_margin)),
|
|
||||||
)
|
|
||||||
|
|
||||||
def set_text(self, text: str):
|
def set_text(self, text: str):
|
||||||
def create_paragraph() -> Paragraph:
|
def create_paragraph() -> Paragraph:
|
||||||
paragraph = Paragraph(self._parent)
|
paragraph = Paragraph(self._parent)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from docx.opc.constants import RELATIONSHIP_TYPE
|
|||||||
from . import Renderable
|
from . import Renderable
|
||||||
from .caption import CaptionInfo
|
from .caption import CaptionInfo
|
||||||
from .image import Image
|
from .image import Image
|
||||||
from .paragraph_sizer import ParagraphSizer
|
from .paragraph_sizer import ParagraphSizerResult, measure_paragraph
|
||||||
from ..layout_tracker import LayoutState
|
from ..layout_tracker import LayoutState
|
||||||
from ..sub_renderable import SubRenderable
|
from ..sub_renderable import SubRenderable
|
||||||
from ..util import create_element
|
from ..util import create_element
|
||||||
@@ -41,7 +41,7 @@ class Link:
|
|||||||
for i, part in enumerate(parts):
|
for i, part in enumerate(parts):
|
||||||
docx_run = DocxRun(create_element("w:r"), self._docx_paragraph)
|
docx_run = DocxRun(create_element("w:r"), self._docx_paragraph)
|
||||||
self._hyperlink.append(docx_run._element)
|
self._hyperlink.append(docx_run._element)
|
||||||
docx_run.text = text
|
docx_run.text = part
|
||||||
docx_run.style = "Hyperlink"
|
docx_run.style = "Hyperlink"
|
||||||
docx_run.bold = is_bold
|
docx_run.bold = is_bold
|
||||||
docx_run.italic = is_italic
|
docx_run.italic = is_italic
|
||||||
@@ -95,6 +95,7 @@ class Paragraph(Renderable):
|
|||||||
self._docx_paragraph.style = "Normal"
|
self._docx_paragraph.style = "Normal"
|
||||||
self._images: list[Image] = []
|
self._images: list[Image] = []
|
||||||
self._pending_refs: list[tuple[object, str, str]] = [] # (run, type, name)
|
self._pending_refs: list[tuple[object, str, str]] = [] # (run, type, name)
|
||||||
|
self._size_cache: dict = {}
|
||||||
|
|
||||||
def add_run(self, text: str, is_bold: bool = None, is_italic: bool = None, color: RGBColor = None,
|
def add_run(self, text: str, is_bold: bool = None, is_italic: bool = None, color: RGBColor = None,
|
||||||
strike_through: bool = None):
|
strike_through: bool = None):
|
||||||
@@ -231,6 +232,13 @@ class Paragraph(Renderable):
|
|||||||
def first_line_indent(self, value: Length):
|
def first_line_indent(self, value: Length):
|
||||||
self._docx_paragraph.paragraph_format.first_line_indent = value
|
self._docx_paragraph.paragraph_format.first_line_indent = value
|
||||||
|
|
||||||
|
def measure(self, previous_paragraph: DocxParagraph | None,
|
||||||
|
max_width: Length) -> ParagraphSizerResult:
|
||||||
|
"""Line count and spacing of this paragraph against a given predecessor."""
|
||||||
|
return measure_paragraph(
|
||||||
|
self._size_cache, self._docx_paragraph, previous_paragraph, max_width
|
||||||
|
)
|
||||||
|
|
||||||
def render(self, previous_rendered: RenderedInfo, layout_state: LayoutState)\
|
def render(self, previous_rendered: RenderedInfo, layout_state: LayoutState)\
|
||||||
-> Generator[RenderedInfo | SubRenderable, None, None]:
|
-> Generator[RenderedInfo | SubRenderable, None, None]:
|
||||||
remaining_space = layout_state.remaining_page_height
|
remaining_space = layout_state.remaining_page_height
|
||||||
@@ -238,11 +246,10 @@ class Paragraph(Renderable):
|
|||||||
if self.page_break_before:
|
if self.page_break_before:
|
||||||
layout_state.add_height(layout_state.remaining_page_height)
|
layout_state.add_height(layout_state.remaining_page_height)
|
||||||
if self._docx_paragraph.text or not self._images:
|
if self._docx_paragraph.text or not self._images:
|
||||||
height_data = ParagraphSizer(
|
height_data = self.measure(
|
||||||
self._docx_paragraph,
|
|
||||||
previous_rendered.docx_element
|
previous_rendered.docx_element
|
||||||
if previous_rendered and isinstance(previous_rendered.docx_element, DocxParagraph) else None,
|
if previous_rendered and isinstance(previous_rendered.docx_element, DocxParagraph) else None,
|
||||||
layout_state.max_width).calculate_height()
|
layout_state.max_width)
|
||||||
|
|
||||||
if layout_state.current_page_height == 0 and layout_state.page > 1:
|
if layout_state.current_page_height == 0 and layout_state.page > 1:
|
||||||
height_data.before = 0
|
height_data.before = 0
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
from copy import deepcopy
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, replace
|
||||||
from functools import cached_property
|
from functools import cached_property, lru_cache
|
||||||
from math import ceil
|
from math import ceil
|
||||||
|
|
||||||
|
from lxml import etree
|
||||||
|
|
||||||
from docx.enum.text import WD_LINE_SPACING
|
from docx.enum.text import WD_LINE_SPACING
|
||||||
from docx.oxml import CT_R
|
from docx.oxml import CT_R
|
||||||
from docx.text.run import Run
|
from docx.text.run import Run
|
||||||
@@ -70,7 +72,7 @@ class Font:
|
|||||||
bbox = self._draw.textbbox((0, 0), text, self._freetypefont)
|
bbox = self._draw.textbbox((0, 0), text, self._freetypefont)
|
||||||
return Pt(bbox[2] - bbox[0])
|
return Pt(bbox[2] - bbox[0])
|
||||||
else:
|
else:
|
||||||
return Pt(len(text) * self._face.glyph.advance.x / 64)
|
return Pt(len(text) * self._advance / 64)
|
||||||
|
|
||||||
def get_line_height(self) -> Length:
|
def get_line_height(self) -> Length:
|
||||||
# TODO: make it work for all fonts
|
# TODO: make it work for all fonts
|
||||||
@@ -80,13 +82,36 @@ class Font:
|
|||||||
return Pt(13.61)
|
return Pt(13.61)
|
||||||
else:
|
else:
|
||||||
return Pt(self._face.size.height / 64)
|
return Pt(self._face.size.height / 64)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def is_mono(self):
|
def _metrics(self) -> tuple[bool, int]:
|
||||||
|
# The advance is snapshotted here instead of being read back from
|
||||||
|
# self._face.glyph later: the face is shared between callers via
|
||||||
|
# _get_font, and any other load_char would otherwise change the glyph
|
||||||
|
# slot under us.
|
||||||
self._face.load_char("i")
|
self._face.load_char("i")
|
||||||
i_width = self._face.glyph.advance.x
|
i_width = self._face.glyph.advance.x
|
||||||
self._face.load_char("m")
|
self._face.load_char("m")
|
||||||
return i_width == self._face.glyph.advance.x
|
m_width = self._face.glyph.advance.x
|
||||||
# return self._face.glyph.bitmap.width
|
return i_width == m_width, m_width
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_mono(self) -> bool:
|
||||||
|
return self._metrics[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _advance(self) -> int:
|
||||||
|
return self._metrics[1]
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=256)
|
||||||
|
def _get_font(name: str, bold: bool, italic: bool, size_pt: float) -> Font:
|
||||||
|
"""Fonts are immutable once built, so identical descriptors share one instance.
|
||||||
|
|
||||||
|
Constructing a Font costs ~2.6 ms (FreeType face + PIL canvas) and
|
||||||
|
calculate_height() would otherwise build 2 + len(runs) of them per call.
|
||||||
|
"""
|
||||||
|
return Font(name, bold, italic, size_pt)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -152,7 +177,7 @@ class ParagraphSizer:
|
|||||||
lines = 1
|
lines = 1
|
||||||
line_width = first_line_indent
|
line_width = first_line_indent
|
||||||
|
|
||||||
space_width = Font(docx_font.name, docx_font.bold, docx_font.italic, docx_font.size.pt).get_text_width(" ")
|
space_width = _get_font(docx_font.name, docx_font.bold, docx_font.italic, docx_font.size.pt).get_text_width(" ")
|
||||||
if not is_mono:
|
if not is_mono:
|
||||||
space_width *= 0.81
|
space_width *= 0.81
|
||||||
|
|
||||||
@@ -168,7 +193,7 @@ class ParagraphSizer:
|
|||||||
docx_font,
|
docx_font,
|
||||||
run.font
|
run.font
|
||||||
)
|
)
|
||||||
font = Font(run_docx_font.name, run_docx_font.bold, run_docx_font.italic, run_docx_font.size.pt)
|
font = _get_font(run_docx_font.name, run_docx_font.bold, run_docx_font.italic, run_docx_font.size.pt)
|
||||||
|
|
||||||
run_text = run.text
|
run_text = run.text
|
||||||
if run_text == "" and run._element.xpath("w:noBreakHyphen"):
|
if run_text == "" and run._element.xpath("w:noBreakHyphen"):
|
||||||
@@ -178,6 +203,11 @@ class ParagraphSizer:
|
|||||||
|
|
||||||
for c in run_text:
|
for c in run_text:
|
||||||
if c == " ":
|
if c == " ":
|
||||||
|
# word_parts_widths[-1] is only ever read here and at the run
|
||||||
|
# boundary below, so the prefix width is measured once per
|
||||||
|
# word instead of once per character.
|
||||||
|
if word_part:
|
||||||
|
word_parts_widths[-1] = font.get_text_width(word_part)
|
||||||
if any(word_parts_widths):
|
if any(word_parts_widths):
|
||||||
width = spaces*space_width + sum(word_parts_widths)
|
width = spaces*space_width + sum(word_parts_widths)
|
||||||
if width <= max_width - line_width:
|
if width <= max_width - line_width:
|
||||||
@@ -200,7 +230,11 @@ class ParagraphSizer:
|
|||||||
spaces += 1
|
spaces += 1
|
||||||
else:
|
else:
|
||||||
word_part += c
|
word_part += c
|
||||||
word_parts_widths[-1] = font.get_text_width(word_part)
|
|
||||||
|
# A word may span runs; the next iteration appends a fresh slot for
|
||||||
|
# the continuation, so this run's fragment has to be measured now.
|
||||||
|
if word_part:
|
||||||
|
word_parts_widths[-1] = font.get_text_width(word_part)
|
||||||
|
|
||||||
return int(lines)
|
return int(lines)
|
||||||
|
|
||||||
@@ -221,7 +255,7 @@ class ParagraphSizer:
|
|||||||
max_width -= (paragraph_format.left_indent or 0) + \
|
max_width -= (paragraph_format.left_indent or 0) + \
|
||||||
(paragraph_format.right_indent or 0)
|
(paragraph_format.right_indent or 0)
|
||||||
|
|
||||||
font = Font(docx_font.name, docx_font.bold, docx_font.italic, docx_font.size.pt)
|
font = _get_font(docx_font.name, docx_font.bold, docx_font.italic, docx_font.size.pt)
|
||||||
|
|
||||||
# here self.paragraph.runs is not used because
|
# here self.paragraph.runs is not used because
|
||||||
# it does not always return all runs (e.g. if they are inside hyperlink)
|
# it does not always return all runs (e.g. if they are inside hyperlink)
|
||||||
@@ -265,3 +299,61 @@ class ParagraphSizer:
|
|||||||
raise NotImplementedError("Line spacing rule AT_LEAST is not supported")
|
raise NotImplementedError("Line spacing rule AT_LEAST is not supported")
|
||||||
|
|
||||||
return ParagraphSizerResult(before, lines, line_height, line_spacing, after)
|
return ParagraphSizerResult(before, lines, line_height, line_spacing, after)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_blank_pPr(pPr) -> bool:
|
||||||
|
"""An empty w:pPr carries no formatting and is equivalent to none at all.
|
||||||
|
|
||||||
|
Measuring a paragraph materialises one: paragraph_format reads go through
|
||||||
|
get_or_add_pPr. Treating it as absent keeps a paragraph's key the same
|
||||||
|
before and after its first measurement.
|
||||||
|
"""
|
||||||
|
return pPr is None or (not len(pPr) and not pPr.attrib)
|
||||||
|
|
||||||
|
|
||||||
|
def _content_key(paragraph: Paragraph) -> bytes:
|
||||||
|
"""Everything calculate_height() reads from the measured paragraph."""
|
||||||
|
p = deepcopy(paragraph._p)
|
||||||
|
if p.pPr is not None and _is_blank_pPr(p.pPr):
|
||||||
|
p.remove(p.pPr)
|
||||||
|
return etree.tostring(p)
|
||||||
|
|
||||||
|
|
||||||
|
def _predecessor_key(paragraph: Paragraph) -> bytes:
|
||||||
|
"""Everything calculate_height() reads from the *preceding* paragraph.
|
||||||
|
|
||||||
|
Only its style and its direct formatting matter (space_after feeds the
|
||||||
|
collapsed space before the measured paragraph), so the runs are left out
|
||||||
|
of the key. That also keeps the key stable once the predecessor has been
|
||||||
|
appended to the body: attaching a w:p makes lxml move the drawing
|
||||||
|
namespace declarations onto it, which would otherwise look like an edit.
|
||||||
|
"""
|
||||||
|
pPr = paragraph._p.pPr
|
||||||
|
return b"" if _is_blank_pPr(pPr) else etree.tostring(deepcopy(pPr))
|
||||||
|
|
||||||
|
|
||||||
|
def measure_paragraph(cache: dict, paragraph: Paragraph,
|
||||||
|
previous_paragraph: Paragraph | None,
|
||||||
|
max_width: Length) -> ParagraphSizerResult:
|
||||||
|
"""calculate_height() memoised per owning renderable.
|
||||||
|
|
||||||
|
The same paragraph is measured several times per document: a table row is
|
||||||
|
peeked before it is built, a listing's first line is probed before the
|
||||||
|
loop, a figure caption is sized once to see whether the figure fits and
|
||||||
|
again when it is emitted. One measurement costs milliseconds, serialising
|
||||||
|
the two paragraphs costs microseconds, so the XML itself is the key — an
|
||||||
|
edit between two calls misses the cache instead of returning a stale
|
||||||
|
height. The cache lives on the caller, so it never spans documents (style
|
||||||
|
definitions are not part of the key).
|
||||||
|
"""
|
||||||
|
key = (
|
||||||
|
_content_key(paragraph),
|
||||||
|
_predecessor_key(previous_paragraph) if previous_paragraph is not None else None,
|
||||||
|
int(max_width),
|
||||||
|
)
|
||||||
|
result = cache.get(key)
|
||||||
|
if result is None:
|
||||||
|
result = ParagraphSizer(paragraph, previous_paragraph, max_width).calculate_height()
|
||||||
|
cache[key] = result
|
||||||
|
# Callers zero out .before on a new page, so hand out a private copy.
|
||||||
|
return replace(result)
|
||||||
|
|||||||
@@ -139,8 +139,6 @@ class Table(Renderable, RequiresNumbering):
|
|||||||
|
|
||||||
def _peek_row_height(self, row_idx: int, layout_state: LayoutState) -> Length:
|
def _peek_row_height(self, row_idx: int, layout_state: LayoutState) -> Length:
|
||||||
"""Estimate row height without attaching cell paragraphs to a table."""
|
"""Estimate row height without attaching cell paragraphs to a table."""
|
||||||
from .paragraph_sizer import ParagraphSizer
|
|
||||||
|
|
||||||
row = self._rows[row_idx]
|
row = self._rows[row_idx]
|
||||||
merge_row = self._merge[row_idx]
|
merge_row = self._merge[row_idx]
|
||||||
row_height = 0
|
row_height = 0
|
||||||
@@ -158,9 +156,9 @@ class Table(Renderable, RequiresNumbering):
|
|||||||
if merge_v != "continue":
|
if merge_v != "continue":
|
||||||
max_w = col_w * span - CELL_OFFSET
|
max_w = col_w * span - CELL_OFFSET
|
||||||
for paragraph in row[col]:
|
for paragraph in row[col]:
|
||||||
cell_height += ParagraphSizer(
|
# Same measurement _build_docx_row() needs later, so the
|
||||||
paragraph._docx_paragraph, None, max_w
|
# cell paragraph is sized once per row width.
|
||||||
).calculate_height().full
|
cell_height += paragraph.measure(None, max_w).full
|
||||||
row_height = max(cell_height, row_height)
|
row_height = max(cell_height, row_height)
|
||||||
col += span
|
col += span
|
||||||
row_height += Pt(0.5) + (
|
row_height += Pt(0.5) + (
|
||||||
@@ -169,24 +167,18 @@ class Table(Renderable, RequiresNumbering):
|
|||||||
return row_height
|
return row_height
|
||||||
|
|
||||||
def _start_block_height(
|
def _start_block_height(
|
||||||
self, previous_rendered: RenderedInfo | None, layout_state: LayoutState
|
self, caption: Caption, previous_rendered: RenderedInfo | None,
|
||||||
|
layout_state: LayoutState
|
||||||
) -> Length:
|
) -> Length:
|
||||||
"""Caption + header + first data row (method: no orphan title/header)."""
|
"""Caption + header + first data row (method: no orphan title/header)."""
|
||||||
from docx.text.paragraph import Paragraph as DocxParagraph
|
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 = (
|
prev_p = (
|
||||||
previous_rendered.docx_element
|
previous_rendered.docx_element
|
||||||
if previous_rendered and isinstance(previous_rendered.docx_element, DocxParagraph)
|
if previous_rendered and isinstance(previous_rendered.docx_element, DocxParagraph)
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
total = ParagraphSizer(
|
total = caption.measure(prev_p, layout_state.max_width).full
|
||||||
cap._docx_paragraph, prev_p, layout_state.max_width
|
|
||||||
).calculate_height().full
|
|
||||||
if self._rows:
|
if self._rows:
|
||||||
total += self._peek_row_height(0, layout_state)
|
total += self._peek_row_height(0, layout_state)
|
||||||
if len(self._rows) > 1:
|
if len(self._rows) > 1:
|
||||||
@@ -234,7 +226,8 @@ class Table(Renderable, RequiresNumbering):
|
|||||||
# not fit, push the whole block to the next page (layout leftover;
|
# not fit, push the whole block to the next page (layout leftover;
|
||||||
# Word also gets keep_with_next on caption/header).
|
# Word also gets keep_with_next on caption/header).
|
||||||
leftover = Pt(0)
|
leftover = Pt(0)
|
||||||
start_h = self._start_block_height(previous_rendered, layout_state)
|
caption = Caption(self._parent, "Таблица", self._caption_info, self._number, True)
|
||||||
|
start_h = self._start_block_height(caption, previous_rendered, layout_state)
|
||||||
if (
|
if (
|
||||||
layout_state.current_page_height > 0
|
layout_state.current_page_height > 0
|
||||||
and start_h > layout_state.remaining_page_height
|
and start_h > layout_state.remaining_page_height
|
||||||
@@ -244,8 +237,7 @@ class Table(Renderable, RequiresNumbering):
|
|||||||
previous_rendered = None
|
previous_rendered = None
|
||||||
|
|
||||||
caption_rendered_infos = list(
|
caption_rendered_infos = list(
|
||||||
Caption(self._parent, "Таблица", self._caption_info, self._number, True)
|
caption.render(previous_rendered, copy(layout_state))
|
||||||
.render(previous_rendered, copy(layout_state))
|
|
||||||
)
|
)
|
||||||
if leftover and caption_rendered_infos:
|
if leftover and caption_rendered_infos:
|
||||||
first = caption_rendered_infos[0]
|
first = caption_rendered_infos[0]
|
||||||
|
|||||||
+7
-5
@@ -27,10 +27,6 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
BOTTOM_MARGIN = Mm(20)
|
BOTTOM_MARGIN = Mm(20)
|
||||||
|
|
||||||
APPENDIX_RE = re.compile(
|
|
||||||
r"^ПРИЛОЖЕНИЕ\s+([А-ЯA-ZЁ])\b",
|
|
||||||
re.IGNORECASE,
|
|
||||||
)
|
|
||||||
# Lettered appendix title: «Приложение А» or «Приложение А Название»
|
# Lettered appendix title: «Приложение А» or «Приложение А Название»
|
||||||
APPENDIX_ITEM_RE = re.compile(
|
APPENDIX_ITEM_RE = re.compile(
|
||||||
r"^ПРИЛОЖЕНИЕ\s+([А-ЯA-ZЁ])(?:\s+(.+))?$",
|
r"^ПРИЛОЖЕНИЕ\s+([А-ЯA-ZЁ])(?:\s+(.+))?$",
|
||||||
@@ -342,7 +338,13 @@ class Renderer:
|
|||||||
try:
|
try:
|
||||||
first = next(infos)
|
first = next(infos)
|
||||||
if isinstance(first, RenderedInfo) and first.height \
|
if isinstance(first, RenderedInfo) and first.height \
|
||||||
>= self._layout_tracker.current_state.remaining_page_height:
|
>= self._layout_tracker.current_state.remaining_page_height \
|
||||||
|
and self._to_new_page:
|
||||||
|
# Deferred figures go in first and move the page boundary,
|
||||||
|
# so this block has to be laid out again against the state
|
||||||
|
# they leave behind. Without anything queued the flush is a
|
||||||
|
# no-op and re-rendering would only rebuild the same
|
||||||
|
# subtree against the same state.
|
||||||
self._flush_to_new_screen()
|
self._flush_to_new_screen()
|
||||||
infos = renderable.render(self.previous_rendered, self._layout_tracker.current_state)
|
infos = renderable.render(self.previous_rendered, self._layout_tracker.current_state)
|
||||||
else:
|
else:
|
||||||
|
|||||||
+89
-148
@@ -3,7 +3,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import shutil
|
import shutil
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -11,12 +10,28 @@ from pathlib import Path
|
|||||||
from md2gost.extended_markdown import markdown
|
from md2gost.extended_markdown import markdown
|
||||||
from md2gost.diagram_renderer import DIAGRAM_LANGS
|
from md2gost.diagram_renderer import DIAGRAM_LANGS
|
||||||
from md2gost.profiles import preprocess_markdown
|
from md2gost.profiles import preprocess_markdown
|
||||||
|
from mdexport import (
|
||||||
|
BlockVisitor,
|
||||||
|
InlineRenderer,
|
||||||
|
is_contents_heading,
|
||||||
|
parse_image_caption,
|
||||||
|
reference_full_name,
|
||||||
|
render_inline,
|
||||||
|
walk_blocks,
|
||||||
|
)
|
||||||
|
|
||||||
from .escape import escape_text, escape_verbatim_for_listing, latex_label
|
from .escape import escape_text, escape_verbatim_for_listing, latex_label
|
||||||
|
|
||||||
|
_REF_PREFIXES = {
|
||||||
|
"рисунок": "fig",
|
||||||
|
"таблица": "tab",
|
||||||
|
"листинг": "lst",
|
||||||
|
"формула": "eq",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class EmitState:
|
class EmitState(BlockVisitor):
|
||||||
lines: list[str] = field(default_factory=list)
|
lines: list[str] = field(default_factory=list)
|
||||||
pending_caption: tuple[str | None, str | None, bool] | None = None # id, text, +listing
|
pending_caption: tuple[str | None, str | None, bool] | None = None # id, text, +listing
|
||||||
biblio: list[tuple[str, str]] = field(default_factory=list)
|
biblio: list[tuple[str, str]] = field(default_factory=list)
|
||||||
@@ -27,57 +42,75 @@ class EmitState:
|
|||||||
def w(self, s: str = "") -> None:
|
def w(self, s: str = "") -> None:
|
||||||
self.lines.append(s)
|
self.lines.append(s)
|
||||||
|
|
||||||
|
def on_heading(self, element) -> None:
|
||||||
|
_emit_heading(self, element)
|
||||||
|
|
||||||
|
def on_table(self, element, caption_id: str | None, caption_text: str | None) -> None:
|
||||||
|
_emit_longtable(self, element, caption_id, caption_text)
|
||||||
|
|
||||||
|
def on_code(self, element, lang: str) -> None:
|
||||||
|
_emit_code(self, element, lang)
|
||||||
|
|
||||||
|
def on_list(self, element) -> None:
|
||||||
|
_emit_list(self, element)
|
||||||
|
|
||||||
|
def on_equation(self, element) -> None:
|
||||||
|
_emit_equation(self, element)
|
||||||
|
|
||||||
|
def on_image(self, image) -> None:
|
||||||
|
_emit_image(self, image)
|
||||||
|
|
||||||
|
def on_body_text(self, children) -> None:
|
||||||
|
text = _inline_text(children).strip()
|
||||||
|
if text:
|
||||||
|
self.w(text)
|
||||||
|
self.w()
|
||||||
|
|
||||||
|
def on_quote(self, element) -> None:
|
||||||
|
self.w(r"\begin{quote}")
|
||||||
|
for child in element.children:
|
||||||
|
if type(child).__name__ == "Paragraph":
|
||||||
|
self.w(_inline_text(child.children))
|
||||||
|
self.w(r"\end{quote}")
|
||||||
|
self.w()
|
||||||
|
|
||||||
|
def on_other(self, element) -> None:
|
||||||
|
self.w(_inline_text(element.children))
|
||||||
|
self.w()
|
||||||
|
|
||||||
|
|
||||||
|
class _LatexInline(InlineRenderer):
|
||||||
|
def escape(self, text: str) -> str:
|
||||||
|
return escape_text(text)
|
||||||
|
|
||||||
|
def code_span(self, node) -> str:
|
||||||
|
return f"\\texttt{{{escape_text(node.children)}}}"
|
||||||
|
|
||||||
|
def emphasis(self, inner: str) -> str:
|
||||||
|
return f"\\textit{{{inner}}}"
|
||||||
|
|
||||||
|
def strong_emphasis(self, inner: str) -> str:
|
||||||
|
return f"\\textbf{{{inner}}}"
|
||||||
|
|
||||||
|
def link(self, node, inner: str) -> str:
|
||||||
|
return f"\\href{{{escape_text(node.dest)}}}{{{inner}}}"
|
||||||
|
|
||||||
|
def reference(self, node) -> str:
|
||||||
|
prefix = _REF_PREFIXES.get(node.type.lower(), "obj")
|
||||||
|
return f"{reference_full_name(node.type)}~\\ref{{{latex_label(node.name, prefix)}}}"
|
||||||
|
|
||||||
|
def inline_equation(self, node) -> str:
|
||||||
|
return f"${node.latex_equation}$"
|
||||||
|
|
||||||
|
def line_break(self) -> str:
|
||||||
|
return " "
|
||||||
|
|
||||||
|
|
||||||
|
_INLINE = _LatexInline()
|
||||||
|
|
||||||
|
|
||||||
def _inline_text(children) -> str:
|
def _inline_text(children) -> str:
|
||||||
parts: list[str] = []
|
return render_inline(children, _INLINE)
|
||||||
for child in children or []:
|
|
||||||
t = type(child).__name__
|
|
||||||
if t in ("RawText", "Literal"):
|
|
||||||
parts.append(escape_text(getattr(child, "children", "") or ""))
|
|
||||||
elif t == "CodeSpan":
|
|
||||||
parts.append(f"\\texttt{{{escape_text(child.children)}}}")
|
|
||||||
elif t == "Emphasis":
|
|
||||||
parts.append(f"\\textit{{{_inline_text(child.children)}}}")
|
|
||||||
elif t == "StrongEmphasis":
|
|
||||||
parts.append(f"\\textbf{{{_inline_text(child.children)}}}")
|
|
||||||
elif t == "Link":
|
|
||||||
parts.append(
|
|
||||||
f"\\href{{{escape_text(child.dest)}}}{{{_inline_text(child.children)}}}"
|
|
||||||
)
|
|
||||||
elif t == "Reference":
|
|
||||||
# @Рисунок:id → \ref{fig:id} with word
|
|
||||||
cat = {
|
|
||||||
"рисунок": "рис.",
|
|
||||||
"таблица": "табл.",
|
|
||||||
"листинг": "лист.",
|
|
||||||
"формула": "форм.",
|
|
||||||
}.get(child.type.lower(), child.type)
|
|
||||||
# Prefer Russian full names as in ГОСТ
|
|
||||||
full = {
|
|
||||||
"рисунок": "Рисунок",
|
|
||||||
"таблица": "Таблица",
|
|
||||||
"листинг": "Листинг",
|
|
||||||
"формула": "формула",
|
|
||||||
}.get(child.type.lower(), child.type)
|
|
||||||
prefix = {
|
|
||||||
"рисунок": "fig",
|
|
||||||
"таблица": "tab",
|
|
||||||
"листинг": "lst",
|
|
||||||
"формула": "eq",
|
|
||||||
}.get(child.type.lower(), "obj")
|
|
||||||
parts.append(f"{full}~\\ref{{{latex_label(child.name, prefix)}}}")
|
|
||||||
elif t == "InlineEquation":
|
|
||||||
parts.append(f"${child.latex_equation}$")
|
|
||||||
elif t == "Image":
|
|
||||||
# handled at block level usually
|
|
||||||
parts.append("")
|
|
||||||
elif t == "LineBreak":
|
|
||||||
parts.append(" ")
|
|
||||||
elif hasattr(child, "children"):
|
|
||||||
parts.append(_inline_text(child.children))
|
|
||||||
else:
|
|
||||||
parts.append(escape_text(str(child)))
|
|
||||||
return "".join(parts)
|
|
||||||
|
|
||||||
|
|
||||||
def _cell_plain(cell) -> str:
|
def _cell_plain(cell) -> str:
|
||||||
@@ -140,7 +173,7 @@ def _emit_heading(st: EmitState, heading) -> None:
|
|||||||
level = heading.level
|
level = heading.level
|
||||||
numbered = getattr(heading, "numbered", True)
|
numbered = getattr(heading, "numbered", True)
|
||||||
|
|
||||||
if upper == "СОДЕРЖАНИЕ" or upper.startswith("СОДЕРЖАНИЕ"):
|
if is_contents_heading(text):
|
||||||
st.skip_toc_heading = True
|
st.skip_toc_heading = True
|
||||||
return # TOC already in tocpage.tex
|
return # TOC already in tocpage.tex
|
||||||
|
|
||||||
@@ -158,48 +191,9 @@ def _emit_heading(st: EmitState, heading) -> None:
|
|||||||
st.w()
|
st.w()
|
||||||
|
|
||||||
|
|
||||||
def _emit_paragraph(st: EmitState, para) -> None:
|
|
||||||
# detect bibliography lines [n]: ...
|
|
||||||
raw = _inline_text(para.children).strip()
|
|
||||||
# Also scan children for plain biblio from original — marko may wrap
|
|
||||||
text_probe = ""
|
|
||||||
for ch in para.children or []:
|
|
||||||
if type(ch).__name__ in ("RawText", "Literal"):
|
|
||||||
text_probe += ch.children or ""
|
|
||||||
m = re.match(r"^\[([^\]]+)\]:\s*(.+)$", text_probe.strip())
|
|
||||||
if m:
|
|
||||||
st.biblio.append((m.group(1), m.group(2).strip()))
|
|
||||||
return
|
|
||||||
|
|
||||||
# inline images in paragraph
|
|
||||||
has_image = any(type(c).__name__ == "Image" for c in (para.children or []))
|
|
||||||
if has_image:
|
|
||||||
for c in para.children:
|
|
||||||
if type(c).__name__ == "Image":
|
|
||||||
_emit_image(st, c)
|
|
||||||
# remaining text
|
|
||||||
others = [c for c in para.children if type(c).__name__ != "Image"]
|
|
||||||
t = _inline_text(others).strip()
|
|
||||||
if t:
|
|
||||||
st.w(t)
|
|
||||||
st.w()
|
|
||||||
return
|
|
||||||
|
|
||||||
if raw:
|
|
||||||
st.w(raw)
|
|
||||||
st.w()
|
|
||||||
|
|
||||||
|
|
||||||
def _emit_image(st: EmitState, img) -> None:
|
def _emit_image(st: EmitState, img) -> None:
|
||||||
dest = img.dest
|
dest = img.dest
|
||||||
title = getattr(img, "title", None) or ""
|
caption_id, caption_text = parse_image_caption(img, getattr(img, "unique_name", None))
|
||||||
# title may be "%id Caption"
|
|
||||||
caption_id = getattr(img, "unique_name", None)
|
|
||||||
caption_text = title
|
|
||||||
if title.startswith("%"):
|
|
||||||
parts = title[1:].split(None, 1)
|
|
||||||
caption_id = parts[0]
|
|
||||||
caption_text = parts[1] if len(parts) > 1 else ""
|
|
||||||
|
|
||||||
# copy image into Images/
|
# copy image into Images/
|
||||||
src = dest
|
src = dest
|
||||||
@@ -230,10 +224,7 @@ def _emit_image(st: EmitState, img) -> None:
|
|||||||
|
|
||||||
def _emit_code(st: EmitState, block, lang: str) -> None:
|
def _emit_code(st: EmitState, block, lang: str) -> None:
|
||||||
source = block.children[0].children if block.children else ""
|
source = block.children[0].children if block.children else ""
|
||||||
cap_id, cap_text, with_listing = (None, None, False)
|
cap_id, cap_text, _with_listing = st.take_caption()
|
||||||
if st.pending_caption:
|
|
||||||
cap_id, cap_text, with_listing = st.pending_caption
|
|
||||||
st.pending_caption = None
|
|
||||||
|
|
||||||
lang_l = (lang or "").strip().lower()
|
lang_l = (lang or "").strip().lower()
|
||||||
if lang_l in DIAGRAM_LANGS:
|
if lang_l in DIAGRAM_LANGS:
|
||||||
@@ -277,10 +268,7 @@ def _emit_list(st: EmitState, lst) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _emit_equation(st: EmitState, eq) -> None:
|
def _emit_equation(st: EmitState, eq) -> None:
|
||||||
cap_id = None
|
cap_id = st.take_caption()[0]
|
||||||
if st.pending_caption:
|
|
||||||
cap_id = st.pending_caption[0]
|
|
||||||
st.pending_caption = None
|
|
||||||
st.w(r"\begin{equation}")
|
st.w(r"\begin{equation}")
|
||||||
if cap_id:
|
if cap_id:
|
||||||
st.w(rf"\label{{{latex_label(cap_id, 'eq')}}}")
|
st.w(rf"\label{{{latex_label(cap_id, 'eq')}}}")
|
||||||
@@ -294,54 +282,7 @@ def emit_document(md_text: str, *, work_dir: Path, images_dir: Path) -> str:
|
|||||||
pre = preprocess_markdown(md_text, emdash_to_hyphen=False)
|
pre = preprocess_markdown(md_text, emdash_to_hyphen=False)
|
||||||
doc = markdown.parse(pre)
|
doc = markdown.parse(pre)
|
||||||
|
|
||||||
for el in doc.children:
|
walk_blocks(doc.children, st)
|
||||||
name = type(el).__name__
|
|
||||||
if name == "BlankLine":
|
|
||||||
continue
|
|
||||||
if name == "Caption":
|
|
||||||
st.pending_caption = (
|
|
||||||
el.unique_name,
|
|
||||||
el.text,
|
|
||||||
getattr(el, "with_listing", False),
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
if name == "Heading":
|
|
||||||
_emit_heading(st, el)
|
|
||||||
continue
|
|
||||||
if name == "Paragraph":
|
|
||||||
_emit_paragraph(st, el)
|
|
||||||
continue
|
|
||||||
if name == "Table":
|
|
||||||
cid = ct = None
|
|
||||||
if st.pending_caption:
|
|
||||||
cid, ct, _ = st.pending_caption
|
|
||||||
st.pending_caption = None
|
|
||||||
_emit_longtable(st, el, cid, ct)
|
|
||||||
continue
|
|
||||||
if name in ("FencedCode", "CodeBlock"):
|
|
||||||
lang = getattr(el, "lang", "") or ""
|
|
||||||
_emit_code(st, el, lang)
|
|
||||||
continue
|
|
||||||
if name == "List":
|
|
||||||
_emit_list(st, el)
|
|
||||||
continue
|
|
||||||
if name == "Equation":
|
|
||||||
_emit_equation(st, el)
|
|
||||||
continue
|
|
||||||
if name == "TOC":
|
|
||||||
continue # tocpage.tex
|
|
||||||
if name == "Quote":
|
|
||||||
st.w(r"\begin{quote}")
|
|
||||||
for ch in el.children:
|
|
||||||
if type(ch).__name__ == "Paragraph":
|
|
||||||
st.w(_inline_text(ch.children))
|
|
||||||
st.w(r"\end{quote}")
|
|
||||||
st.w()
|
|
||||||
continue
|
|
||||||
# fallback
|
|
||||||
if hasattr(el, "children"):
|
|
||||||
st.w(_inline_text(el.children))
|
|
||||||
st.w()
|
|
||||||
|
|
||||||
if st.biblio:
|
if st.biblio:
|
||||||
st.w(r"\begin{thebibliography}{99}")
|
st.w(r"\begin{thebibliography}{99}")
|
||||||
|
|||||||
+2
-8
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
from mdexport.labels import sanitize_label
|
||||||
|
|
||||||
_SPECIAL = {
|
_SPECIAL = {
|
||||||
"\\": r"\textbackslash{}",
|
"\\": r"\textbackslash{}",
|
||||||
@@ -32,11 +32,5 @@ def escape_verbatim_for_listing(s: str) -> str:
|
|||||||
return s.replace("\\end{lstlisting}", "\\end\\{lstlisting}")
|
return s.replace("\\end{lstlisting}", "\\end\\{lstlisting}")
|
||||||
|
|
||||||
|
|
||||||
_LABEL_RE = re.compile(r"[^\w\-]+", re.UNICODE)
|
|
||||||
|
|
||||||
|
|
||||||
def latex_label(name: str | None, prefix: str = "obj") -> str:
|
def latex_label(name: str | None, prefix: str = "obj") -> str:
|
||||||
if not name:
|
return sanitize_label(name, prefix, ":")
|
||||||
return prefix
|
|
||||||
cleaned = _LABEL_RE.sub("-", name.strip()).strip("-")
|
|
||||||
return f"{prefix}:{cleaned or 'x'}"
|
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"""
|
||||||
|
Shared AST-walking building blocks for the md2gost export backends.
|
||||||
|
|
||||||
|
`md2latex` and `md2fodt` consume the same extended-markdown AST produced by
|
||||||
|
`md2gost.extended_markdown` and differ only in how each node is serialised.
|
||||||
|
This package holds the traversal and the backend-independent parsing so the
|
||||||
|
two emitters keep only their formatting code.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .blocks import (
|
||||||
|
BlockVisitor,
|
||||||
|
PendingCaption,
|
||||||
|
caption_tuple,
|
||||||
|
is_contents_heading,
|
||||||
|
match_bibliography,
|
||||||
|
parse_image_caption,
|
||||||
|
walk_blocks,
|
||||||
|
walk_paragraph,
|
||||||
|
)
|
||||||
|
from .inline import (
|
||||||
|
REFERENCE_FULL_NAMES,
|
||||||
|
InlineRenderer,
|
||||||
|
reference_full_name,
|
||||||
|
render_inline,
|
||||||
|
)
|
||||||
|
from .labels import sanitize_label
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"REFERENCE_FULL_NAMES",
|
||||||
|
"BlockVisitor",
|
||||||
|
"InlineRenderer",
|
||||||
|
"PendingCaption",
|
||||||
|
"caption_tuple",
|
||||||
|
"is_contents_heading",
|
||||||
|
"match_bibliography",
|
||||||
|
"parse_image_caption",
|
||||||
|
"reference_full_name",
|
||||||
|
"render_inline",
|
||||||
|
"sanitize_label",
|
||||||
|
"walk_blocks",
|
||||||
|
"walk_paragraph",
|
||||||
|
]
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
"""Backend-agnostic traversal of the top level of a md2gost markdown document."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
#: `(unique_name, text, with_listing)` captured from a `%id Подпись` line.
|
||||||
|
PendingCaption = tuple[str | None, str | None, bool]
|
||||||
|
|
||||||
|
_BIBLIO_RE = re.compile(r"^\[([^\]]+)\]:\s*(.+)$")
|
||||||
|
|
||||||
|
_CONTENTS_TITLE = "СОДЕРЖАНИЕ"
|
||||||
|
|
||||||
|
|
||||||
|
def caption_tuple(element) -> PendingCaption:
|
||||||
|
return (element.unique_name, element.text, getattr(element, "with_listing", False))
|
||||||
|
|
||||||
|
|
||||||
|
def match_bibliography(children) -> tuple[str, str] | None:
|
||||||
|
"""Detect a `[key]: описание` source line, ignoring formatted spans."""
|
||||||
|
probe = ""
|
||||||
|
for child in children or []:
|
||||||
|
if type(child).__name__ in ("RawText", "Literal"):
|
||||||
|
probe += child.children or ""
|
||||||
|
m = _BIBLIO_RE.match(probe.strip())
|
||||||
|
if not m:
|
||||||
|
return None
|
||||||
|
return m.group(1), m.group(2).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def is_contents_heading(text: str) -> bool:
|
||||||
|
upper = text.upper()
|
||||||
|
return upper == _CONTENTS_TITLE or upper.startswith(_CONTENTS_TITLE)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_image_caption(image, default_id: str | None) -> tuple[str | None, str]:
|
||||||
|
"""Split an image title of the form `%id Подпись` into id and caption text."""
|
||||||
|
title = getattr(image, "title", None) or ""
|
||||||
|
if not title.startswith("%"):
|
||||||
|
return default_id, title
|
||||||
|
parts = title[1:].split(None, 1)
|
||||||
|
return parts[0], parts[1] if len(parts) > 1 else ""
|
||||||
|
|
||||||
|
|
||||||
|
class BlockVisitor(ABC):
|
||||||
|
"""
|
||||||
|
Emit hooks for :func:`walk_blocks` and :func:`walk_paragraph`.
|
||||||
|
|
||||||
|
Implementations own `pending_caption` and `biblio` because the concrete
|
||||||
|
emitters need to read them outside the walk as well.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pending_caption: PendingCaption | None
|
||||||
|
biblio: list[tuple[str, str]]
|
||||||
|
|
||||||
|
def take_caption(self) -> PendingCaption:
|
||||||
|
pending = self.pending_caption
|
||||||
|
if pending is None:
|
||||||
|
return (None, None, False)
|
||||||
|
self.pending_caption = None
|
||||||
|
return pending
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def on_heading(self, element) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def on_table(self, element, caption_id: str | None, caption_text: str | None) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def on_code(self, element, lang: str) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def on_list(self, element) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def on_equation(self, element) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def on_quote(self, element) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def on_other(self, element) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def on_image(self, image) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def on_body_text(self, children) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
def on_biblio(self, key: str, text: str) -> None:
|
||||||
|
self.biblio.append((key, text))
|
||||||
|
|
||||||
|
def on_paragraph(self, element) -> None:
|
||||||
|
walk_paragraph(element, self)
|
||||||
|
|
||||||
|
|
||||||
|
def walk_paragraph(paragraph, visitor: BlockVisitor) -> None:
|
||||||
|
"""Route one paragraph to the bibliography, image or plain-text hook."""
|
||||||
|
biblio = match_bibliography(paragraph.children)
|
||||||
|
if biblio is not None:
|
||||||
|
visitor.on_biblio(*biblio)
|
||||||
|
return
|
||||||
|
|
||||||
|
children = paragraph.children or []
|
||||||
|
images = [c for c in children if type(c).__name__ == "Image"]
|
||||||
|
if images:
|
||||||
|
for image in images:
|
||||||
|
visitor.on_image(image)
|
||||||
|
visitor.on_body_text([c for c in children if type(c).__name__ != "Image"])
|
||||||
|
return
|
||||||
|
|
||||||
|
visitor.on_body_text(children)
|
||||||
|
|
||||||
|
|
||||||
|
def walk_blocks(children, visitor: BlockVisitor) -> None:
|
||||||
|
"""Dispatch every top-level element of a parsed document to `visitor`."""
|
||||||
|
for element in children:
|
||||||
|
name = type(element).__name__
|
||||||
|
if name in ("BlankLine", "TOC"):
|
||||||
|
continue
|
||||||
|
if name == "Caption":
|
||||||
|
visitor.pending_caption = caption_tuple(element)
|
||||||
|
elif name == "Heading":
|
||||||
|
visitor.on_heading(element)
|
||||||
|
elif name == "Paragraph":
|
||||||
|
visitor.on_paragraph(element)
|
||||||
|
elif name == "Table":
|
||||||
|
caption_id, caption_text, _ = visitor.take_caption()
|
||||||
|
visitor.on_table(element, caption_id, caption_text)
|
||||||
|
elif name in ("FencedCode", "CodeBlock"):
|
||||||
|
visitor.on_code(element, getattr(element, "lang", "") or "")
|
||||||
|
elif name == "List":
|
||||||
|
visitor.on_list(element)
|
||||||
|
elif name == "Equation":
|
||||||
|
visitor.on_equation(element)
|
||||||
|
elif name == "Quote":
|
||||||
|
visitor.on_quote(element)
|
||||||
|
elif hasattr(element, "children"):
|
||||||
|
visitor.on_other(element)
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
"""Backend-agnostic traversal of md2gost inline AST nodes."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
#: Wording used when a `@Тип:id` reference is spelled out, per ГОСТ 7.32.
|
||||||
|
REFERENCE_FULL_NAMES = {
|
||||||
|
"рисунок": "Рисунок",
|
||||||
|
"таблица": "Таблица",
|
||||||
|
"листинг": "Листинг",
|
||||||
|
"формула": "формула",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def reference_full_name(ref_type: str) -> str:
|
||||||
|
return REFERENCE_FULL_NAMES.get(ref_type.lower(), ref_type)
|
||||||
|
|
||||||
|
|
||||||
|
class InlineRenderer(ABC):
|
||||||
|
"""
|
||||||
|
Formatting hooks for :func:`render_inline`.
|
||||||
|
|
||||||
|
Defaults are provided only for the nodes every backend renders the same
|
||||||
|
way; everything else is abstract so a new backend cannot silently drop a
|
||||||
|
node type.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def escape(self, text: str) -> str:
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def code_span(self, node) -> str:
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def emphasis(self, inner: str) -> str:
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def strong_emphasis(self, inner: str) -> str:
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def link(self, node, inner: str) -> str:
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def reference(self, node) -> str:
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def inline_equation(self, node) -> str:
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def line_break(self) -> str:
|
||||||
|
...
|
||||||
|
|
||||||
|
def raw_text(self, text: str) -> str:
|
||||||
|
return self.escape(text)
|
||||||
|
|
||||||
|
def image(self, node) -> str:
|
||||||
|
return "" # images carry a caption, so they are emitted at block level
|
||||||
|
|
||||||
|
def unknown(self, node) -> str:
|
||||||
|
return self.escape(str(node))
|
||||||
|
|
||||||
|
|
||||||
|
def render_inline(children, renderer: InlineRenderer) -> str:
|
||||||
|
"""Render a list of inline nodes into a backend-specific string."""
|
||||||
|
parts: list[str] = []
|
||||||
|
for child in children or []:
|
||||||
|
node_type = type(child).__name__
|
||||||
|
if node_type in ("RawText", "Literal"):
|
||||||
|
parts.append(renderer.raw_text(getattr(child, "children", "") or ""))
|
||||||
|
elif node_type == "CodeSpan":
|
||||||
|
parts.append(renderer.code_span(child))
|
||||||
|
elif node_type == "Emphasis":
|
||||||
|
parts.append(renderer.emphasis(render_inline(child.children, renderer)))
|
||||||
|
elif node_type == "StrongEmphasis":
|
||||||
|
parts.append(renderer.strong_emphasis(render_inline(child.children, renderer)))
|
||||||
|
elif node_type == "Link":
|
||||||
|
parts.append(renderer.link(child, render_inline(child.children, renderer)))
|
||||||
|
elif node_type == "Reference":
|
||||||
|
parts.append(renderer.reference(child))
|
||||||
|
elif node_type == "InlineEquation":
|
||||||
|
parts.append(renderer.inline_equation(child))
|
||||||
|
elif node_type == "Image":
|
||||||
|
parts.append(renderer.image(child))
|
||||||
|
elif node_type == "LineBreak":
|
||||||
|
parts.append(renderer.line_break())
|
||||||
|
elif hasattr(child, "children"):
|
||||||
|
parts.append(render_inline(child.children, renderer))
|
||||||
|
else:
|
||||||
|
parts.append(renderer.unknown(child))
|
||||||
|
return "".join(parts)
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
"""Cross-reference slugs shared by the export backends."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
_LABEL_RE = re.compile(r"[^\w\-]+", re.UNICODE)
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_label(name: str | None, prefix: str = "obj", separator: str = ":") -> str:
|
||||||
|
"""
|
||||||
|
Turn a `%id` caption name into a target usable as an anchor.
|
||||||
|
|
||||||
|
`separator` differs per backend: LaTeX `\\label` accepts a colon, an
|
||||||
|
XML `NCName` does not.
|
||||||
|
"""
|
||||||
|
if not name:
|
||||||
|
return prefix
|
||||||
|
cleaned = _LABEL_RE.sub("-", name.strip()).strip("-")
|
||||||
|
return f"{prefix}{separator}{cleaned or 'x'}"
|
||||||
@@ -1,241 +0,0 @@
|
|||||||
# md_to_docx_gost_final.py
|
|
||||||
from docx import Document
|
|
||||||
from docx.shared import Pt, Mm, RGBColor, Inches
|
|
||||||
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING
|
|
||||||
from docx.oxml.ns import qn
|
|
||||||
from docx.oxml import OxmlElement
|
|
||||||
from docx.enum.table import WD_TABLE_ALIGNMENT
|
|
||||||
import re
|
|
||||||
import requests
|
|
||||||
from io import BytesIO
|
|
||||||
|
|
||||||
# Счётчик для листингов
|
|
||||||
listing_counter = 0
|
|
||||||
|
|
||||||
|
|
||||||
def setup_gost_style(doc):
|
|
||||||
style = doc.styles['Normal']
|
|
||||||
font = style.font
|
|
||||||
font.name = 'Times New Roman'
|
|
||||||
font.size = Pt(14)
|
|
||||||
font.color.rgb = RGBColor(0, 0, 0) # Чёрный цвет
|
|
||||||
|
|
||||||
rFonts = style.element.rPr.get_or_add_rFonts()
|
|
||||||
rFonts.set(qn('w:eastAsia'), 'Times New Roman')
|
|
||||||
rFonts.set(qn('w:cs'), 'Times New Roman')
|
|
||||||
|
|
||||||
pf = style.paragraph_format
|
|
||||||
pf.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
|
|
||||||
pf.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
|
||||||
pf.first_line_indent = Mm(1.25)
|
|
||||||
pf.space_before = Pt(0)
|
|
||||||
pf.space_after = Pt(0)
|
|
||||||
|
|
||||||
section = doc.sections[0]
|
|
||||||
section.top_margin = Mm(20)
|
|
||||||
section.bottom_margin = Mm(20)
|
|
||||||
section.left_margin = Mm(30)
|
|
||||||
section.right_margin = Mm(15)
|
|
||||||
|
|
||||||
|
|
||||||
def add_heading(doc, text, level=1):
|
|
||||||
heading = doc.add_heading(text, level=level)
|
|
||||||
if level <= 3:
|
|
||||||
heading.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
||||||
for run in heading.runs:
|
|
||||||
run.font.bold = True
|
|
||||||
run.font.size = Pt(16) if level == 1 else Pt(14)
|
|
||||||
run.font.color.rgb = RGBColor(0, 0, 0)
|
|
||||||
|
|
||||||
|
|
||||||
def add_listing_caption(doc, language="C++"):
|
|
||||||
global listing_counter
|
|
||||||
listing_counter += 1
|
|
||||||
p = doc.add_paragraph(f"Листинг {listing_counter} — Код на {language}", style='Caption')
|
|
||||||
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
||||||
p.style.font.color.rgb = RGBColor(0, 0, 0)
|
|
||||||
for run in p.runs:
|
|
||||||
run.font.color.rgb = RGBColor(0, 0, 0)
|
|
||||||
|
|
||||||
|
|
||||||
def add_code_block_as_table(doc, code_lines, language=""):
|
|
||||||
global listing_counter
|
|
||||||
add_listing_caption(doc, language) # Подпись ДО таблицы
|
|
||||||
|
|
||||||
table = doc.add_table(rows=1, cols=1)
|
|
||||||
table.style = 'Table Grid'
|
|
||||||
table.alignment = WD_TABLE_ALIGNMENT.CENTER
|
|
||||||
cell = table.cell(0, 0)
|
|
||||||
|
|
||||||
# Серый фон
|
|
||||||
shading_elm = parse_xml(f'<w:shd {nsdecls("w")} w:fill="D9D9D9"/>')
|
|
||||||
cell._tc.get_or_add_tcPr().append(shading_elm)
|
|
||||||
|
|
||||||
paragraph = cell.paragraphs[0]
|
|
||||||
run = paragraph.add_run()
|
|
||||||
run.font.name = 'Courier New'
|
|
||||||
run._element.rPr.rFonts.set(qn('w:eastAsia'), 'Courier New')
|
|
||||||
run.font.size = Pt(12)
|
|
||||||
run.font.color.rgb = RGBColor(0, 0, 0)
|
|
||||||
run.text = "\n".join(code_lines).strip()
|
|
||||||
|
|
||||||
# Запрещаем разрыв строки внутри первой строки таблицы
|
|
||||||
tr = table.rows[0]._tr
|
|
||||||
trPr = tr.get_or_add_trPr()
|
|
||||||
cantSplit = OxmlElement('w:cantSplit')
|
|
||||||
cantSplit.set(qn('w:val'), 'true')
|
|
||||||
trPr.append(cantSplit)
|
|
||||||
|
|
||||||
|
|
||||||
def add_image_from_url(doc, url, caption_text):
|
|
||||||
try:
|
|
||||||
response = requests.get(url.strip(), timeout=5)
|
|
||||||
image_stream = BytesIO(response.content)
|
|
||||||
p = doc.add_picture(image_stream, width=Inches(5))
|
|
||||||
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
||||||
|
|
||||||
# Подпись к рисунку
|
|
||||||
caption = doc.add_paragraph(f"Рисунок — {caption_text}", style='Caption')
|
|
||||||
caption.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
||||||
for run in caption.runs:
|
|
||||||
run.font.color.rgb = RGBColor(0, 0, 0)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"⚠️ Ошибка загрузки изображения: {url} — {e}")
|
|
||||||
doc.add_paragraph(f"[Изображение не загружено: {caption_text}]").alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
||||||
|
|
||||||
|
|
||||||
def parse_xml(xml):
|
|
||||||
from docx.oxml import parse_xml as ox_parse
|
|
||||||
return ox_parse(xml)
|
|
||||||
|
|
||||||
|
|
||||||
def nsdecls(*namespaces):
|
|
||||||
return ' '.join(f'xmlns:{ns}="http://schemas.openxmlformats.org/wordprocessingml/2006/main"' for ns in namespaces)
|
|
||||||
|
|
||||||
|
|
||||||
def add_formatted_paragraph(doc, text):
|
|
||||||
p = doc.add_paragraph()
|
|
||||||
p.style = 'Normal'
|
|
||||||
|
|
||||||
parts = re.split(r'(\*\*[^*]+\*\*)', text)
|
|
||||||
for part in parts:
|
|
||||||
if part.startswith('**') and part.endswith('**'):
|
|
||||||
clean_text = part[2:-2]
|
|
||||||
run = p.add_run(clean_text)
|
|
||||||
run.bold = True
|
|
||||||
run.font.color.rgb = RGBColor(0, 0, 0)
|
|
||||||
elif part.strip():
|
|
||||||
run = p.add_run(part)
|
|
||||||
run.font.color.rgb = RGBColor(0, 0, 0)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_markdown_and_fill(doc, md_text):
|
|
||||||
global listing_counter
|
|
||||||
lines = md_text.split('\n')
|
|
||||||
i = 0
|
|
||||||
in_code_block = False
|
|
||||||
code_buffer = []
|
|
||||||
code_language = ""
|
|
||||||
|
|
||||||
while i < len(lines):
|
|
||||||
line = lines[i].rstrip()
|
|
||||||
|
|
||||||
# === Заголовки 1–4 ===
|
|
||||||
if line.startswith('#### '):
|
|
||||||
if in_code_block:
|
|
||||||
add_code_block_as_table(doc, code_buffer, code_language)
|
|
||||||
in_code_block = False
|
|
||||||
code_buffer = []
|
|
||||||
add_heading(doc, line[5:].strip(), level=4)
|
|
||||||
|
|
||||||
elif line.startswith('### '):
|
|
||||||
if in_code_block:
|
|
||||||
add_code_block_as_table(doc, code_buffer, code_language)
|
|
||||||
in_code_block = False
|
|
||||||
code_buffer = []
|
|
||||||
add_heading(doc, line[4:].strip(), level=3)
|
|
||||||
|
|
||||||
elif line.startswith('## '):
|
|
||||||
if in_code_block:
|
|
||||||
add_code_block_as_table(doc, code_buffer, code_language)
|
|
||||||
in_code_block = False
|
|
||||||
code_buffer = []
|
|
||||||
add_heading(doc, line[3:].strip(), level=2)
|
|
||||||
|
|
||||||
elif line.startswith('# '):
|
|
||||||
if in_code_block:
|
|
||||||
add_code_block_as_table(doc, code_buffer, code_language)
|
|
||||||
in_code_block = False
|
|
||||||
code_buffer = []
|
|
||||||
add_heading(doc, line[2:].strip(), level=1)
|
|
||||||
|
|
||||||
# === Горизонтальная линия ===
|
|
||||||
elif line.strip() == '---':
|
|
||||||
doc.add_paragraph().add_run().add_break()
|
|
||||||
|
|
||||||
# === Блок кода ``` ===
|
|
||||||
elif line.startswith('```'):
|
|
||||||
if not in_code_block:
|
|
||||||
in_code_block = True
|
|
||||||
code_language = line[3:].strip() or "C++"
|
|
||||||
code_buffer = []
|
|
||||||
else:
|
|
||||||
in_code_block = False
|
|
||||||
add_code_block_as_table(doc, code_buffer, code_language)
|
|
||||||
code_buffer = []
|
|
||||||
|
|
||||||
elif in_code_block:
|
|
||||||
code_buffer.append(line)
|
|
||||||
|
|
||||||
# === Изображения:  ===
|
|
||||||
elif re.match(r'^!\[.*?\]\(.*?\)$', line.strip()):
|
|
||||||
match = re.match(r'^!\[(.*?)\]\((.*?)\)$', line.strip())
|
|
||||||
if match:
|
|
||||||
caption = match.group(1)
|
|
||||||
url = match.group(2)
|
|
||||||
add_image_from_url(doc, url, caption)
|
|
||||||
|
|
||||||
# === Маркированный список ===
|
|
||||||
elif line.startswith('- '):
|
|
||||||
p = doc.add_paragraph(line[2:], style='List Bullet')
|
|
||||||
for run in p.runs:
|
|
||||||
run.font.color.rgb = RGBColor(0, 0, 0)
|
|
||||||
|
|
||||||
# === Обычный абзац с жирным ===
|
|
||||||
else:
|
|
||||||
if line.strip():
|
|
||||||
if '**' in line:
|
|
||||||
add_formatted_paragraph(doc, line)
|
|
||||||
else:
|
|
||||||
p = doc.add_paragraph(line)
|
|
||||||
p.style = 'Normal'
|
|
||||||
for run in p.runs:
|
|
||||||
run.font.color.rgb = RGBColor(0, 0, 0)
|
|
||||||
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
# Остаток кода
|
|
||||||
if in_code_block and code_buffer:
|
|
||||||
add_code_block_as_table(doc, code_buffer, code_language)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
try:
|
|
||||||
with open(r"C:\Users\hlebushek\Desktop\Пример.txt", "r", encoding="utf-8") as f:
|
|
||||||
md_content = f.read()
|
|
||||||
except FileNotFoundError:
|
|
||||||
print("Файл 'Пример.txt' не найден.")
|
|
||||||
return
|
|
||||||
|
|
||||||
doc = Document()
|
|
||||||
setup_gost_style(doc)
|
|
||||||
parse_markdown_and_fill(doc, md_content)
|
|
||||||
|
|
||||||
output_filename = "Практическая_работа_6.docx"
|
|
||||||
doc.save(output_filename)
|
|
||||||
print(f"✅ Готово! Файл сохранён как '{output_filename}'")
|
|
||||||
print(f"📎 Включены: изображения, чёрный текст, листинги перед кодом, нумерация.")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -1,284 +0,0 @@
|
|||||||
# md_to_docx_gost_final.py
|
|
||||||
from docx import Document
|
|
||||||
from docx.shared import Pt, Mm, RGBColor, Inches
|
|
||||||
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING
|
|
||||||
from docx.oxml.ns import qn
|
|
||||||
from docx.oxml import OxmlElement
|
|
||||||
from docx.enum.table import WD_TABLE_ALIGNMENT
|
|
||||||
import json
|
|
||||||
import re
|
|
||||||
import requests
|
|
||||||
from io import BytesIO
|
|
||||||
|
|
||||||
import parser
|
|
||||||
|
|
||||||
# Счётчик листингов
|
|
||||||
listing_counter = 0
|
|
||||||
|
|
||||||
|
|
||||||
def setup_gost_style(doc):
|
|
||||||
style = doc.styles['Normal']
|
|
||||||
font = style.font
|
|
||||||
font.name = 'Times New Roman'
|
|
||||||
font.size = Pt(14)
|
|
||||||
font.color.rgb = RGBColor(0, 0, 0) # Чёрный цвет
|
|
||||||
|
|
||||||
rFonts = style.element.rPr.get_or_add_rFonts()
|
|
||||||
rFonts.set(qn('w:eastAsia'), 'Times New Roman')
|
|
||||||
rFonts.set(qn('w:cs'), 'Times New Roman')
|
|
||||||
|
|
||||||
pf = style.paragraph_format
|
|
||||||
pf.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
|
|
||||||
pf.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
|
||||||
pf.first_line_indent = Mm(1.25)
|
|
||||||
pf.space_before = Pt(0)
|
|
||||||
pf.space_after = Pt(0)
|
|
||||||
|
|
||||||
section = doc.sections[0]
|
|
||||||
section.top_margin = Mm(20)
|
|
||||||
section.bottom_margin = Mm(20)
|
|
||||||
section.left_margin = Mm(30)
|
|
||||||
section.right_margin = Mm(15)
|
|
||||||
|
|
||||||
|
|
||||||
def add_heading(doc, text, level=1):
|
|
||||||
heading = doc.add_heading(text, level=level)
|
|
||||||
if level <= 3:
|
|
||||||
heading.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
||||||
for run in heading.runs:
|
|
||||||
run.font.bold = True
|
|
||||||
run.font.size = Pt(16) if level == 1 else Pt(14)
|
|
||||||
run.font.color.rgb = RGBColor(0, 0, 0)
|
|
||||||
|
|
||||||
|
|
||||||
def add_listing_caption(doc, language="", is_continuation=False):
|
|
||||||
global listing_counter
|
|
||||||
p = doc.add_paragraph(style='Caption')
|
|
||||||
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
||||||
run = p.add_run()
|
|
||||||
run.font.color.rgb = RGBColor(0, 0, 0)
|
|
||||||
run.font.name = 'Times New Roman'
|
|
||||||
run._element.rPr.rFonts.set(qn('w:cs'), 'Times New Roman')
|
|
||||||
|
|
||||||
if is_continuation:
|
|
||||||
run.text = f"Продолжение листинга {listing_counter} — Код на {language}"
|
|
||||||
else:
|
|
||||||
listing_counter += 1
|
|
||||||
run.text = f"Листинг {listing_counter} — Код на {language}"
|
|
||||||
|
|
||||||
|
|
||||||
def add_code_block(doc, code_lines, language=""):
|
|
||||||
global listing_counter
|
|
||||||
add_listing_caption(doc, language, is_continuation=False)
|
|
||||||
|
|
||||||
# Создаём таблицу для кода с выравниванием по левому краю
|
|
||||||
table = doc.add_table(rows=0, cols=1)
|
|
||||||
table.style = 'Table Grid'
|
|
||||||
table.alignment = WD_TABLE_ALIGNMENT.LEFT # Важно: выравнивание по левому краю
|
|
||||||
|
|
||||||
# Добавляем каждую строку кода как отдельную строку таблицы
|
|
||||||
for line in code_lines:
|
|
||||||
row = table.add_row()
|
|
||||||
cell = row.cells[0]
|
|
||||||
cell.text = line.strip()
|
|
||||||
|
|
||||||
# Серый фон для ячейки
|
|
||||||
shading_elm = parse_xml(f'<w:shd {nsdecls("w")} w:fill="D9D9D9"/>')
|
|
||||||
cell._tc.get_or_add_tcPr().append(shading_elm)
|
|
||||||
|
|
||||||
# Настройка параграфа внутри ячейки
|
|
||||||
for paragraph in cell.paragraphs:
|
|
||||||
paragraph_format = paragraph.paragraph_format
|
|
||||||
paragraph_format.left_indent = Mm(0) # Убираем левый отступ
|
|
||||||
paragraph_format.right_indent = Mm(0)
|
|
||||||
paragraph_format.first_line_indent = Mm(0)
|
|
||||||
paragraph_format.alignment = WD_ALIGN_PARAGRAPH.LEFT # Выравнивание по левому краю
|
|
||||||
|
|
||||||
for run in paragraph.runs:
|
|
||||||
run.font.name = 'Courier New'
|
|
||||||
run._element.rPr.rFonts.set(qn('w:eastAsia'), 'Courier New')
|
|
||||||
run.font.size = Pt(12)
|
|
||||||
run.font.color.rgb = RGBColor(0, 0, 0)
|
|
||||||
|
|
||||||
# Добавляем подпись о продолжении, если код длинный
|
|
||||||
if len(code_lines) > 25:
|
|
||||||
p = doc.add_paragraph()
|
|
||||||
run = p.add_run(f"Продолжение листинга {listing_counter} — Код на {language}")
|
|
||||||
run.italic = True
|
|
||||||
run.font.size = Pt(12)
|
|
||||||
run.font.color.rgb = RGBColor(80, 80, 80)
|
|
||||||
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
||||||
p.space_before = Pt(6)
|
|
||||||
p.space_after = Pt(6)
|
|
||||||
|
|
||||||
|
|
||||||
def add_image_from_url(doc, url, caption_text):
|
|
||||||
try:
|
|
||||||
response = requests.get(url.strip(), timeout=5)
|
|
||||||
image_stream = BytesIO(response.content)
|
|
||||||
p = doc.add_picture(image_stream, width=Inches(5))
|
|
||||||
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
||||||
|
|
||||||
caption = doc.add_paragraph(f"Рисунок — {caption_text}", style='Caption')
|
|
||||||
caption.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
||||||
for run in caption.runs:
|
|
||||||
run.font.color.rgb = RGBColor(0, 0, 0)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"⚠️ Ошибка загрузки изображения: {url} — {e}")
|
|
||||||
doc.add_paragraph(f"[Изображение не загружено: {caption_text}]").alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
||||||
|
|
||||||
|
|
||||||
def parse_xml(xml):
|
|
||||||
from docx.oxml import parse_xml as ox_parse
|
|
||||||
return ox_parse(xml)
|
|
||||||
|
|
||||||
|
|
||||||
def nsdecls(*namespaces):
|
|
||||||
return ' '.join(f'xmlns:{ns}="http://schemas.openxmlformats.org/wordprocessingml/2006/main"' for ns in namespaces)
|
|
||||||
|
|
||||||
|
|
||||||
def process_element(doc, element, depth=0):
|
|
||||||
"""Рекурсивная обработка элемента с учётом вложенности"""
|
|
||||||
global listing_counter
|
|
||||||
|
|
||||||
if element['type'] == 'blank_line':
|
|
||||||
return
|
|
||||||
|
|
||||||
elif element['type'] == 'heading':
|
|
||||||
level = element['attrs']['level']
|
|
||||||
text = ''.join(extract_text(element['children']))
|
|
||||||
add_heading(doc, text, level=level)
|
|
||||||
|
|
||||||
elif element['type'] == 'paragraph':
|
|
||||||
p = doc.add_paragraph()
|
|
||||||
p.style = 'Normal'
|
|
||||||
|
|
||||||
# Обработка вложенных элементов в параграфе
|
|
||||||
for child in element['children']:
|
|
||||||
if child['type'] == 'text':
|
|
||||||
p.add_run(child['raw'])
|
|
||||||
elif child['type'] == 'strong':
|
|
||||||
text = ''.join(extract_text(child['children']))
|
|
||||||
run = p.add_run(text)
|
|
||||||
run.bold = True
|
|
||||||
elif child['type'] == 'codespan':
|
|
||||||
run = p.add_run(child['raw'])
|
|
||||||
run.font.name = 'Courier New'
|
|
||||||
run.font.size = Pt(12)
|
|
||||||
elif child['type'] == 'linebreak':
|
|
||||||
p.add_run().add_break()
|
|
||||||
elif child['type'] == 'image':
|
|
||||||
# Изображение обрабатывается отдельно
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Установка отступов в зависимости от глубины
|
|
||||||
pf = p.paragraph_format
|
|
||||||
pf.left_indent = Mm(5 * depth) if depth > 0 else Mm(0)
|
|
||||||
|
|
||||||
elif element['type'] == 'thematic_break':
|
|
||||||
doc.add_paragraph().add_run().add_break()
|
|
||||||
|
|
||||||
elif element['type'] == 'list':
|
|
||||||
# Обработка списка с учётом глубины вложенности
|
|
||||||
for item in element['children']:
|
|
||||||
if item['type'] == 'list_item':
|
|
||||||
# Создаём маркированный пункт
|
|
||||||
p = doc.add_paragraph(style='List Bullet')
|
|
||||||
p.paragraph_format.left_indent = Mm(5 * depth)
|
|
||||||
p.paragraph_format.first_line_indent = Mm(-3.5) # Отступ для маркера
|
|
||||||
|
|
||||||
# Обрабатываем содержимое пункта списка
|
|
||||||
for child in item['children']:
|
|
||||||
if child['type'] == 'block_text':
|
|
||||||
for subchild in child['children']:
|
|
||||||
if subchild['type'] == 'text':
|
|
||||||
p.add_run(subchild['raw'])
|
|
||||||
elif subchild['type'] == 'strong':
|
|
||||||
text = ''.join(extract_text(subchild['children']))
|
|
||||||
run = p.add_run(text)
|
|
||||||
run.bold = True
|
|
||||||
elif subchild['type'] == 'codespan':
|
|
||||||
run = p.add_run(subchild['raw'])
|
|
||||||
run.font.name = 'Courier New'
|
|
||||||
run.font.size = Pt(12)
|
|
||||||
|
|
||||||
# Рекурсивная обработка вложенных списков
|
|
||||||
for child in item['children']:
|
|
||||||
if child['type'] == 'list':
|
|
||||||
process_element(doc, child, depth=depth + 1)
|
|
||||||
|
|
||||||
elif element['type'] == 'block_code':
|
|
||||||
code_lines = element['raw'].split('\n')
|
|
||||||
language = element['attrs'].get('info', 'plain')
|
|
||||||
add_code_block(doc, code_lines, language)
|
|
||||||
|
|
||||||
elif element['type'] == 'image':
|
|
||||||
url = element['attrs']['url']
|
|
||||||
caption = ''.join(extract_text(element['children']))
|
|
||||||
add_image_from_url(doc, url, caption)
|
|
||||||
|
|
||||||
# Обработка других типов элементов
|
|
||||||
elif element['type'] == 'list_item':
|
|
||||||
# Обработка отдельных пунктов списка (для рекурсии)
|
|
||||||
p = doc.add_paragraph(style='List Bullet')
|
|
||||||
p.paragraph_format.left_indent = Mm(5 * depth)
|
|
||||||
p.paragraph_format.first_line_indent = Mm(-3.5)
|
|
||||||
|
|
||||||
for child in element['children']:
|
|
||||||
if child['type'] == 'block_text':
|
|
||||||
for subchild in child['children']:
|
|
||||||
if subchild['type'] == 'text':
|
|
||||||
p.add_run(subchild['raw'])
|
|
||||||
elif subchild['type'] == 'strong':
|
|
||||||
text = ''.join(extract_text(subchild['children']))
|
|
||||||
run = p.add_run(text)
|
|
||||||
run.bold = True
|
|
||||||
elif subchild['type'] == 'codespan':
|
|
||||||
run = p.add_run(subchild['raw'])
|
|
||||||
run.font.name = 'Courier New'
|
|
||||||
run.font.size = Pt(12)
|
|
||||||
|
|
||||||
# Обработка вложенных списков
|
|
||||||
for child in element['children']:
|
|
||||||
if child['type'] == 'list':
|
|
||||||
process_element(doc, child, depth=depth + 1)
|
|
||||||
|
|
||||||
|
|
||||||
def extract_text(elements):
|
|
||||||
"""Извлекает текст из структурированных элементов"""
|
|
||||||
texts = []
|
|
||||||
for el in elements:
|
|
||||||
if el['type'] == 'text':
|
|
||||||
texts.append(el['raw'])
|
|
||||||
elif 'children' in el:
|
|
||||||
texts.extend(extract_text(el['children']))
|
|
||||||
return texts
|
|
||||||
|
|
||||||
|
|
||||||
def parse_structured_data_and_fill(doc, structured_data):
|
|
||||||
"""Основная функция обработки структурированных данных"""
|
|
||||||
global listing_counter
|
|
||||||
listing_counter = 0 # Сброс счётчика листингов
|
|
||||||
|
|
||||||
for element in structured_data:
|
|
||||||
process_element(doc, element)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
structured_data = parser.mdToraw()
|
|
||||||
# Создаём документ
|
|
||||||
doc = Document()
|
|
||||||
setup_gost_style(doc)
|
|
||||||
|
|
||||||
# Обрабатываем структурированные данные
|
|
||||||
parse_structured_data_and_fill(doc, structured_data)
|
|
||||||
|
|
||||||
# Сохраняем результат
|
|
||||||
output_filename = "Практическая_работа_6.docx"
|
|
||||||
doc.save(output_filename)
|
|
||||||
print(f"✅ Документ сохранён как '{output_filename}'")
|
|
||||||
print(f"📎 Обработано {len(structured_data)} элементов структурированных данных")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -1,340 +0,0 @@
|
|||||||
import mistune
|
|
||||||
|
|
||||||
text= """
|
|
||||||
## Практическая работа 6 (часть 2)
|
|
||||||
**Тема. Нелинейные структуры данных. Бинарное дерево.**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 1. Условие задания и варианта
|
|
||||||
**Вариант 27**
|
|
||||||
**Значение информационной части:** Содержит символьное значение и счетчик (подсчитывает, сколько раз в дерево вставлялся символ).
|
|
||||||
**Операции варианта:**
|
|
||||||
1. Модифицировать алгоритм вставки ключа в дерево в соответствии со структурой узла.
|
|
||||||
2. Определить символ, который встретился большее количество раз.
|
|
||||||
3. Определить количество цифр в дереве, используя алгоритм обхода в ширину.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. Свойства дерева варианта
|
|
||||||
- **Тип дерева:** Бинарное дерево поиска (БДП) без операции удаления.
|
|
||||||
- **Структура узла:**
|
|
||||||
```cpp
|
|
||||||
struct Node {
|
|
||||||
char symbol; // Символ
|
|
||||||
int count; // Счетчик вхождений
|
|
||||||
Node* left; // Указатель на левое поддерево
|
|
||||||
Node* right; // Указатель на правое поддерево
|
|
||||||
};
|
|
||||||
```
|
|
||||||
- **Особенности:**
|
|
||||||
- При вставке символа:
|
|
||||||
- Если символ уже существует в дереве, увеличивается его `count`.
|
|
||||||
- Иначе создается новый узел с `count = 1`.
|
|
||||||
- Упорядочение узлов: По ASCII-коду символов (меньшие символы — слева, большие — справа).
|
|
||||||
- Операция удаления не реализована (требования варианта 24–30).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. Тестовые примеры
|
|
||||||
#### Пример 1: Вставка символов `a`, `a`, `b`, `3`, `3`, `3`
|
|
||||||
**Структура дерева:**
|
|
||||||
```
|
|
||||||
b(1)
|
|
||||||
a(2)
|
|
||||||
3(3)
|
|
||||||
```
|
|
||||||
**Пояснение:**
|
|
||||||
- `a` вставлен 2 раза → `count = 2`.
|
|
||||||
- `3` вставлен 3 раза → `count = 3` (самый частый символ).
|
|
||||||
- Количество цифр: 3 (только символ `3`).
|
|
||||||
|
|
||||||
#### Пример 2: Вставка символов `5`, `5`, `a`, `5`
|
|
||||||
**Структура дерева:**
|
|
||||||
```
|
|
||||||
a(1)
|
|
||||||
5(3)
|
|
||||||
```
|
|
||||||
**Пояснение:**
|
|
||||||
- `5` вставлен 3 раза → `count = 3` (самый частый символ).
|
|
||||||
- Количество цифр: 3.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4. Описание функций
|
|
||||||
|
|
||||||
#### 4.1. Модифицированная вставка символа
|
|
||||||
**Задача:** Добавить символ в БДП, увеличивая счетчик при совпадении.
|
|
||||||
**Предусловие:** Дерево инициализировано.
|
|
||||||
**Постусловие:** Символ добавлен или обновлен в дереве.
|
|
||||||
**Прототип:**
|
|
||||||
```cpp
|
|
||||||
void BinaryTree::insert(char symbol);
|
|
||||||
```
|
|
||||||
**Алгоритм:**
|
|
||||||
1. Начать с корня.
|
|
||||||
2. Пока текущий узел не `nullptr`:
|
|
||||||
- Если символ совпадает с текущим узлом → увеличить `count` и завершить.
|
|
||||||
- Если символ меньше текущего → перейти в левое поддерево.
|
|
||||||
- Иначе → перейти в правое поддерево.
|
|
||||||
3. Если узел не найден → создать новый узел с `count = 1`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 4.2. Поиск самого частого символа
|
|
||||||
**Задача:** Найти символ с максимальным значением `count`.
|
|
||||||
**Предусловие:** Дерево не пустое.
|
|
||||||
**Постусловие:** Возвращен символ и его счетчик.
|
|
||||||
**Прототип:**
|
|
||||||
```cpp
|
|
||||||
std::pair<char, int> BinaryTree::findMostFrequent();
|
|
||||||
```
|
|
||||||
**Алгоритм:**
|
|
||||||
1. Инициализировать `maxCount = 0`, `maxSymbol = '\\0'`.
|
|
||||||
2. Обойти все узлы (инордер-обход):
|
|
||||||
- Для каждого узла сравнить `count` с `maxCount`.
|
|
||||||
- Если `count > maxCount` → обновить `maxSymbol` и `maxCount`.
|
|
||||||
3. Вернуть пару `(maxSymbol, maxCount)`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 4.3. Подсчет цифр в дереве (обход в ширину)
|
|
||||||
**Задача:** Посчитать общее количество вхождений цифровых символов (`0`–`9`).
|
|
||||||
**Предусловие:** Дерево инициализировано.
|
|
||||||
**Постусловие:** Возвращено число — сумма `count` для всех цифровых символов.
|
|
||||||
**Прототип:**
|
|
||||||
```cpp
|
|
||||||
int BinaryTree::countDigits();
|
|
||||||
```
|
|
||||||
**Алгоритм:**
|
|
||||||
1. Инициализировать очередь и счетчик `total = 0`.
|
|
||||||
2. Поместить корень в очередь.
|
|
||||||
3. Пока очередь не пуста:
|
|
||||||
- Извлечь узел из очереди.
|
|
||||||
- Если символ узла — цифра (`'0'`–`'9'`) → добавить `count` к `total`.
|
|
||||||
- Поместить левый и правый дочерние узлы в очередь.
|
|
||||||
4. Вернуть `total`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 4.4. Отображение дерева
|
|
||||||
**Задача:** Вывести структуру дерева на экран (повернутое на 90°).
|
|
||||||
**Предусловие:** Дерево инициализировано.
|
|
||||||
**Постусловие:** Дерево отображено в консоли.
|
|
||||||
**Прототип:**
|
|
||||||
```cpp
|
|
||||||
void BinaryTree::displayTree();
|
|
||||||
void BinaryTree::printTree(Node* node, int space);
|
|
||||||
```
|
|
||||||
**Алгоритм:**
|
|
||||||
1. Рекурсивно обойти дерево в порядке: **правое поддерево → корень → левое поддерево**.
|
|
||||||
2. Для каждого уровня увеличивать отступ (на 10 пробелов).
|
|
||||||
3. Вывести символ и `count` с текущим отступом.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 5. Код программы
|
|
||||||
```cpp
|
|
||||||
#include <iostream>
|
|
||||||
#include <stack>
|
|
||||||
#include <queue>
|
|
||||||
using namespace std;
|
|
||||||
|
|
||||||
struct Node {
|
|
||||||
char symbol;
|
|
||||||
int count;
|
|
||||||
Node* left;
|
|
||||||
Node* right;
|
|
||||||
Node(char s) : symbol(s), count(1), left(nullptr), right(nullptr) {}
|
|
||||||
};
|
|
||||||
|
|
||||||
class BinaryTree {
|
|
||||||
private:
|
|
||||||
Node* root;
|
|
||||||
|
|
||||||
void printTree(Node* node, int space) {
|
|
||||||
if (node == nullptr) return;
|
|
||||||
space += 10;
|
|
||||||
printTree(node->right, space);
|
|
||||||
cout << endl;
|
|
||||||
for (int i = 10; i < space; i++) cout << " ";
|
|
||||||
cout << node->symbol << "(" << node->count << ")";
|
|
||||||
printTree(node->left, space);
|
|
||||||
}
|
|
||||||
|
|
||||||
public:
|
|
||||||
BinaryTree() : root(nullptr) {}
|
|
||||||
|
|
||||||
void insert(char symbol) {
|
|
||||||
Node** current = &root;
|
|
||||||
while (*current != nullptr) {
|
|
||||||
if (symbol == (*current)->symbol) {
|
|
||||||
(*current)->count++;
|
|
||||||
return;
|
|
||||||
} else if (symbol < (*current)->symbol) {
|
|
||||||
current = &((*current)->left);
|
|
||||||
} else {
|
|
||||||
current = &((*current)->right);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*current = new Node(symbol);
|
|
||||||
}
|
|
||||||
|
|
||||||
pair<char, int> findMostFrequent() {
|
|
||||||
if (root == nullptr) return make_pair('\\0', 0);
|
|
||||||
char maxSymbol = '\\0';
|
|
||||||
int maxCount = 0;
|
|
||||||
stack<Node*> s;
|
|
||||||
Node* current = root;
|
|
||||||
while (current != nullptr || !s.empty()) {
|
|
||||||
while (current != nullptr) {
|
|
||||||
s.push(current);
|
|
||||||
current = current->left;
|
|
||||||
}
|
|
||||||
current = s.top();
|
|
||||||
s.pop();
|
|
||||||
if (current->count > maxCount) {
|
|
||||||
maxCount = current->count;
|
|
||||||
maxSymbol = current->symbol;
|
|
||||||
}
|
|
||||||
current = current->right;
|
|
||||||
}
|
|
||||||
return make_pair(maxSymbol, maxCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
int countDigits() {
|
|
||||||
if (root == nullptr) return 0;
|
|
||||||
int total = 0;
|
|
||||||
queue<Node*> q;
|
|
||||||
q.push(root);
|
|
||||||
while (!q.empty()) {
|
|
||||||
Node* current = q.front();
|
|
||||||
q.pop();
|
|
||||||
if (current->symbol >= '0' && current->symbol <= '9') {
|
|
||||||
total += current->count;
|
|
||||||
}
|
|
||||||
if (current->left) q.push(current->left);
|
|
||||||
if (current->right) q.push(current->right);
|
|
||||||
}
|
|
||||||
return total;
|
|
||||||
}
|
|
||||||
|
|
||||||
void displayTree() {
|
|
||||||
printTree(root, 0);
|
|
||||||
cout << "\n\n";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
BinaryTree tree;
|
|
||||||
int choice;
|
|
||||||
char symbol;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
cout << "\n1. Insert symbol\n";
|
|
||||||
cout << "2. Display tree\n";
|
|
||||||
cout << "3. Find most frequent symbol\n";
|
|
||||||
cout << "4. Count digits\n";
|
|
||||||
cout << "5. Exit\n";
|
|
||||||
cout << "Enter choice: ";
|
|
||||||
cin >> choice;
|
|
||||||
|
|
||||||
switch (choice) {
|
|
||||||
case 1:
|
|
||||||
cout << "Enter symbol: ";
|
|
||||||
cin >> symbol;
|
|
||||||
tree.insert(symbol);
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
cout << "Tree structure:\n";
|
|
||||||
tree.displayTree();
|
|
||||||
break;
|
|
||||||
case 3: {
|
|
||||||
auto result = tree.findMostFrequent();
|
|
||||||
if (result.second == 0) {
|
|
||||||
cout << "Tree is empty.\n";
|
|
||||||
} else {
|
|
||||||
cout << "Most frequent symbol: " << result.first
|
|
||||||
<< " (count: " << result.second << ")\n";
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 4:
|
|
||||||
cout << "Number of digits: " << tree.countDigits() << endl;
|
|
||||||
break;
|
|
||||||
case 5:
|
|
||||||
return 0;
|
|
||||||
default:
|
|
||||||
cout << "Invalid choice.\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 6. Скриншоты тестирования
|
|
||||||
#### Тест 1: Вставка `a`, `a`, `b`, `3`, `3`, `3`
|
|
||||||

|
|
||||||
- **Вывод дерева:** Корректно отображена структура с `3(3)` как самым частым символом.
|
|
||||||
- **Количество цифр:** 3 (символ `3` вставлен 3 раза).
|
|
||||||
|
|
||||||
#### Тест 2: Вставка `5`, `5`, `a`, `5`
|
|
||||||

|
|
||||||
- **Самый частый символ:** `5` (count=3).
|
|
||||||
- **Количество цифр:** 3.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 7. Ответы на контрольные вопросы
|
|
||||||
1. **Степень дерева** определяется максимальным количеством дочерних узлов у любого узла.
|
|
||||||
2. **Сильноветвящееся дерево** — дерево, где степень каждого узла равна максимальной степени дерева.
|
|
||||||
3. **Путь в дереве** — последовательность узлов от корня к целевому узлу.
|
|
||||||
4. **Длина пути** — количество ребер в пути (на 1 меньше количества узлов).
|
|
||||||
5. **Степень бинарного дерева** равна 2 (максимум 2 дочерних узла на узел).
|
|
||||||
6. **Пустое дерево** допустимо (корень = `nullptr`).
|
|
||||||
7. **Бинарное дерево** — дерево, где каждый узел имеет не более 2 дочерних узлов.
|
|
||||||
8. **Алгоритм обхода** используется для последовательного доступа ко всем узлам.
|
|
||||||
9. **Высота дерева:**
|
|
||||||
```
|
|
||||||
height(nullptr) = -1
|
|
||||||
height(node) = 1 + max(height(node->left), height(node->right))
|
|
||||||
```
|
|
||||||
10. **Дерево по таблице:**
|
|
||||||

|
|
||||||
11. **Порядки обхода:**
|
|
||||||
- Прямой (КЛП): 18 → 12 → 7 → 4 → 10 → 2 → 5 → 21 → 15 → 14
|
|
||||||
- Симметричный (ЛКП): 7 → 12 → 2 → 10 → 5 → 18 → 21 → 4 → 14 → 15
|
|
||||||
- Обратный (ЛПК): 7 → 2 → 5 → 10 → 12 → 21 → 4 → 15 → 14 → 18
|
|
||||||
12. **Обход в ширину** использует **очередь**.
|
|
||||||
13. **Пример обхода в ширину:** 18 → 12 → 14 → 7 → 10 → 15 → 4 → 2 → 5 → 21.
|
|
||||||
14. **Не рекурсивный обход в глубину** использует **стек**.
|
|
||||||
15. **Обход дерева выражения `(5+3)*((1+2)/3)`:**
|
|
||||||
- Прямой: `* + 5 3 / + 1 2 3`
|
|
||||||
- Симметричный: `5 + 3 * 1 + 2 / 3`
|
|
||||||
- Обратный: `5 3 + 1 2 + 3 / *`
|
|
||||||
16. **Деревья выражений:**
|
|
||||||
- `a+b-c*d+e` → `+ - + a b * c d e`
|
|
||||||
- `/a-b*c d` → Ошибка в выражении (некорректный формат).
|
|
||||||
- `a b c d / - *` → `* a - b / c d`
|
|
||||||
- `* - / + a b c d e` → `* - / + a b c d e`
|
|
||||||
17. **Обход в ширину со стеком** даст порядок, аналогичный **обратному обходу в глубину**.
|
|
||||||
18. **БДП для симметричного обхода `40 45 46 50 65 70 75`:**
|
|
||||||

|
|
||||||
19. **Исходное дерево для прямого обхода `50 45 35 15 40 46 65 75 70`:**
|
|
||||||

|
|
||||||
20. **БДП после вставки `1, 48, 75, 100`:**
|
|
||||||

|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Вывод:** Реализованы все операции для бинарного дерева поиска с модифицированной структурой узла. Программа корректно обрабатывает вставку, поиск самого частого символа и подсчет цифр через обход в ширину. Тесты подтверждают работоспособность алгоритмов.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def mdToraw():
|
|
||||||
markdown = mistune.create_markdown(renderer='ast')
|
|
||||||
#markdown = mistune.create_markdown()
|
|
||||||
return markdown(text)
|
|
||||||
|
|
||||||
print(mdToraw())
|
|
||||||
Generated
-791
@@ -1,791 +0,0 @@
|
|||||||
# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "babel"
|
|
||||||
version = "2.12.1"
|
|
||||||
description = "Internationalization utilities"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
files = [
|
|
||||||
{file = "Babel-2.12.1-py3-none-any.whl", hash = "sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610"},
|
|
||||||
{file = "Babel-2.12.1.tar.gz", hash = "sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "certifi"
|
|
||||||
version = "2023.7.22"
|
|
||||||
description = "Python package for providing Mozilla's CA Bundle."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.6"
|
|
||||||
files = [
|
|
||||||
{file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"},
|
|
||||||
{file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "charset-normalizer"
|
|
||||||
version = "3.2.0"
|
|
||||||
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7.0"
|
|
||||||
files = [
|
|
||||||
{file = "charset-normalizer-3.2.0.tar.gz", hash = "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp310-cp310-win32.whl", hash = "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp311-cp311-win32.whl", hash = "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp37-cp37m-win32.whl", hash = "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp38-cp38-win32.whl", hash = "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp39-cp39-win32.whl", hash = "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9"},
|
|
||||||
{file = "charset_normalizer-3.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80"},
|
|
||||||
{file = "charset_normalizer-3.2.0-py3-none-any.whl", hash = "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "contourpy"
|
|
||||||
version = "1.1.0"
|
|
||||||
description = "Python library for calculating contours of 2D quadrilateral grids"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "contourpy-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:89f06eff3ce2f4b3eb24c1055a26981bffe4e7264acd86f15b97e40530b794bc"},
|
|
||||||
{file = "contourpy-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dffcc2ddec1782dd2f2ce1ef16f070861af4fb78c69862ce0aab801495dda6a3"},
|
|
||||||
{file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25ae46595e22f93592d39a7eac3d638cda552c3e1160255258b695f7b58e5655"},
|
|
||||||
{file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17cfaf5ec9862bc93af1ec1f302457371c34e688fbd381f4035a06cd47324f48"},
|
|
||||||
{file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18a64814ae7bce73925131381603fff0116e2df25230dfc80d6d690aa6e20b37"},
|
|
||||||
{file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c81f22b4f572f8a2110b0b741bb64e5a6427e0a198b2cdc1fbaf85f352a3aa"},
|
|
||||||
{file = "contourpy-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:53cc3a40635abedbec7f1bde60f8c189c49e84ac180c665f2cd7c162cc454baa"},
|
|
||||||
{file = "contourpy-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:1f795597073b09d631782e7245016a4323cf1cf0b4e06eef7ea6627e06a37ff2"},
|
|
||||||
{file = "contourpy-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0b7b04ed0961647691cfe5d82115dd072af7ce8846d31a5fac6c142dcce8b882"},
|
|
||||||
{file = "contourpy-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27bc79200c742f9746d7dd51a734ee326a292d77e7d94c8af6e08d1e6c15d545"},
|
|
||||||
{file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:052cc634bf903c604ef1a00a5aa093c54f81a2612faedaa43295809ffdde885e"},
|
|
||||||
{file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9382a1c0bc46230fb881c36229bfa23d8c303b889b788b939365578d762b5c18"},
|
|
||||||
{file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5cec36c5090e75a9ac9dbd0ff4a8cf7cecd60f1b6dc23a374c7d980a1cd710e"},
|
|
||||||
{file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f0cbd657e9bde94cd0e33aa7df94fb73c1ab7799378d3b3f902eb8eb2e04a3a"},
|
|
||||||
{file = "contourpy-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:181cbace49874f4358e2929aaf7ba84006acb76694102e88dd15af861996c16e"},
|
|
||||||
{file = "contourpy-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fb3b7d9e6243bfa1efb93ccfe64ec610d85cfe5aec2c25f97fbbd2e58b531256"},
|
|
||||||
{file = "contourpy-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bcb41692aa09aeb19c7c213411854402f29f6613845ad2453d30bf421fe68fed"},
|
|
||||||
{file = "contourpy-1.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5d123a5bc63cd34c27ff9c7ac1cd978909e9c71da12e05be0231c608048bb2ae"},
|
|
||||||
{file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62013a2cf68abc80dadfd2307299bfa8f5aa0dcaec5b2954caeb5fa094171103"},
|
|
||||||
{file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0b6616375d7de55797d7a66ee7d087efe27f03d336c27cf1f32c02b8c1a5ac70"},
|
|
||||||
{file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:317267d915490d1e84577924bd61ba71bf8681a30e0d6c545f577363157e5e94"},
|
|
||||||
{file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d551f3a442655f3dcc1285723f9acd646ca5858834efeab4598d706206b09c9f"},
|
|
||||||
{file = "contourpy-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e7a117ce7df5a938fe035cad481b0189049e8d92433b4b33aa7fc609344aafa1"},
|
|
||||||
{file = "contourpy-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:d4f26b25b4f86087e7d75e63212756c38546e70f2a92d2be44f80114826e1cd4"},
|
|
||||||
{file = "contourpy-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc00bb4225d57bff7ebb634646c0ee2a1298402ec10a5fe7af79df9a51c1bfd9"},
|
|
||||||
{file = "contourpy-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:189ceb1525eb0655ab8487a9a9c41f42a73ba52d6789754788d1883fb06b2d8a"},
|
|
||||||
{file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f2931ed4741f98f74b410b16e5213f71dcccee67518970c42f64153ea9313b9"},
|
|
||||||
{file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30f511c05fab7f12e0b1b7730ebdc2ec8deedcfb505bc27eb570ff47c51a8f15"},
|
|
||||||
{file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:143dde50520a9f90e4a2703f367cf8ec96a73042b72e68fcd184e1279962eb6f"},
|
|
||||||
{file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e94bef2580e25b5fdb183bf98a2faa2adc5b638736b2c0a4da98691da641316a"},
|
|
||||||
{file = "contourpy-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ed614aea8462735e7d70141374bd7650afd1c3f3cb0c2dbbcbe44e14331bf002"},
|
|
||||||
{file = "contourpy-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:438ba416d02f82b692e371858143970ed2eb6337d9cdbbede0d8ad9f3d7dd17d"},
|
|
||||||
{file = "contourpy-1.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a698c6a7a432789e587168573a864a7ea374c6be8d4f31f9d87c001d5a843493"},
|
|
||||||
{file = "contourpy-1.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:397b0ac8a12880412da3551a8cb5a187d3298a72802b45a3bd1805e204ad8439"},
|
|
||||||
{file = "contourpy-1.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:a67259c2b493b00e5a4d0f7bfae51fb4b3371395e47d079a4446e9b0f4d70e76"},
|
|
||||||
{file = "contourpy-1.1.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2b836d22bd2c7bb2700348e4521b25e077255ebb6ab68e351ab5aa91ca27e027"},
|
|
||||||
{file = "contourpy-1.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084eaa568400cfaf7179b847ac871582199b1b44d5699198e9602ecbbb5f6104"},
|
|
||||||
{file = "contourpy-1.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:911ff4fd53e26b019f898f32db0d4956c9d227d51338fb3b03ec72ff0084ee5f"},
|
|
||||||
{file = "contourpy-1.1.0.tar.gz", hash = "sha256:e53046c3863828d21d531cc3b53786e6580eb1ba02477e8681009b6aa0870b21"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
numpy = ">=1.16"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
bokeh = ["bokeh", "selenium"]
|
|
||||||
docs = ["furo", "sphinx-copybutton"]
|
|
||||||
mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.2.0)", "types-Pillow"]
|
|
||||||
test = ["Pillow", "contourpy[test-no-images]", "matplotlib"]
|
|
||||||
test-no-images = ["pytest", "pytest-cov", "wurlitzer"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cycler"
|
|
||||||
version = "0.11.0"
|
|
||||||
description = "Composable style cycles"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.6"
|
|
||||||
files = [
|
|
||||||
{file = "cycler-0.11.0-py3-none-any.whl", hash = "sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3"},
|
|
||||||
{file = "cycler-0.11.0.tar.gz", hash = "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "docxcompose"
|
|
||||||
version = "1.4.0"
|
|
||||||
description = "Compose .docx documents"
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
files = [
|
|
||||||
{file = "docxcompose-1.4.0.tar.gz", hash = "sha256:bcf2799a0b63c29eb77a3d799a2f28443ae0f69f8691ff3d753f706be515c3e9"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
babel = "*"
|
|
||||||
lxml = "*"
|
|
||||||
python-docx = ">=0.8.8"
|
|
||||||
setuptools = "*"
|
|
||||||
six = "*"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
test = ["pytest"]
|
|
||||||
tests = ["pytest"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fonttools"
|
|
||||||
version = "4.41.1"
|
|
||||||
description = "Tools to manipulate font files"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "fonttools-4.41.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a7bbb290d13c6dd718ec2c3db46fe6c5f6811e7ea1e07f145fd8468176398224"},
|
|
||||||
{file = "fonttools-4.41.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ec453a45778524f925a8f20fd26a3326f398bfc55d534e37bab470c5e415caa1"},
|
|
||||||
{file = "fonttools-4.41.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2071267deaa6d93cb16288613419679c77220543551cbe61da02c93d92df72f"},
|
|
||||||
{file = "fonttools-4.41.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e3334d51f0e37e2c6056e67141b2adabc92613a968797e2571ca8a03bd64773"},
|
|
||||||
{file = "fonttools-4.41.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cac73bbef7734e78c60949da11c4903ee5837168e58772371bd42a75872f4f82"},
|
|
||||||
{file = "fonttools-4.41.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:edee0900cf0eedb29d17c7876102d6e5a91ee333882b1f5abc83e85b934cadb5"},
|
|
||||||
{file = "fonttools-4.41.1-cp310-cp310-win32.whl", hash = "sha256:2a22b2c425c698dcd5d6b0ff0b566e8e9663172118db6fd5f1941f9b8063da9b"},
|
|
||||||
{file = "fonttools-4.41.1-cp310-cp310-win_amd64.whl", hash = "sha256:547ab36a799dded58a46fa647266c24d0ed43a66028cd1cd4370b246ad426cac"},
|
|
||||||
{file = "fonttools-4.41.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:849ec722bbf7d3501a0e879e57dec1fc54919d31bff3f690af30bb87970f9784"},
|
|
||||||
{file = "fonttools-4.41.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:38cdecd8f1fd4bf4daae7fed1b3170dfc1b523388d6664b2204b351820aa78a7"},
|
|
||||||
{file = "fonttools-4.41.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ae64303ba670f8959fdaaa30ba0c2dabe75364fdec1caeee596c45d51ca3425"},
|
|
||||||
{file = "fonttools-4.41.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f14f3ccea4cc7dd1b277385adf3c3bf18f9860f87eab9c2fb650b0af16800f55"},
|
|
||||||
{file = "fonttools-4.41.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:33191f062549e6bb1a4782c22a04ebd37009c09360e2d6686ac5083774d06d95"},
|
|
||||||
{file = "fonttools-4.41.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:704bccd69b0abb6fab9f5e4d2b75896afa48b427caa2c7988792a2ffce35b441"},
|
|
||||||
{file = "fonttools-4.41.1-cp311-cp311-win32.whl", hash = "sha256:4edc795533421e98f60acee7d28fc8d941ff5ac10f44668c9c3635ad72ae9045"},
|
|
||||||
{file = "fonttools-4.41.1-cp311-cp311-win_amd64.whl", hash = "sha256:aaaef294d8e411f0ecb778a0aefd11bb5884c9b8333cc1011bdaf3b58ca4bd75"},
|
|
||||||
{file = "fonttools-4.41.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3d1f9471134affc1e3b1b806db6e3e2ad3fa99439e332f1881a474c825101096"},
|
|
||||||
{file = "fonttools-4.41.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:59eba8b2e749a1de85760da22333f3d17c42b66e03758855a12a2a542723c6e7"},
|
|
||||||
{file = "fonttools-4.41.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9b3cc10dc9e0834b6665fd63ae0c6964c6bc3d7166e9bc84772e0edd09f9fa2"},
|
|
||||||
{file = "fonttools-4.41.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da2c2964bdc827ba6b8a91dc6de792620be4da3922c4cf0599f36a488c07e2b2"},
|
|
||||||
{file = "fonttools-4.41.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7763316111df7b5165529f4183a334aa24c13cdb5375ffa1dc8ce309c8bf4e5c"},
|
|
||||||
{file = "fonttools-4.41.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b2d1ee95be42b80d1f002d1ee0a51d7a435ea90d36f1a5ae331be9962ee5a3f1"},
|
|
||||||
{file = "fonttools-4.41.1-cp38-cp38-win32.whl", hash = "sha256:f48602c0b3fd79cd83a34c40af565fe6db7ac9085c8823b552e6e751e3a5b8be"},
|
|
||||||
{file = "fonttools-4.41.1-cp38-cp38-win_amd64.whl", hash = "sha256:b0938ebbeccf7c80bb9a15e31645cf831572c3a33d5cc69abe436e7000c61b14"},
|
|
||||||
{file = "fonttools-4.41.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e5c2b0a95a221838991e2f0e455dec1ca3a8cc9cd54febd68cc64d40fdb83669"},
|
|
||||||
{file = "fonttools-4.41.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:891cfc5a83b0307688f78b9bb446f03a7a1ad981690ac8362f50518bc6153975"},
|
|
||||||
{file = "fonttools-4.41.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73ef0bb5d60eb02ba4d3a7d23ada32184bd86007cb2de3657cfcb1175325fc83"},
|
|
||||||
{file = "fonttools-4.41.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f240d9adf0583ac8fc1646afe7f4ac039022b6f8fa4f1575a2cfa53675360b69"},
|
|
||||||
{file = "fonttools-4.41.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bdd729744ae7ecd7f7311ad25d99da4999003dcfe43b436cf3c333d4e68de73d"},
|
|
||||||
{file = "fonttools-4.41.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b927e5f466d99c03e6e20961946314b81d6e3490d95865ef88061144d9f62e38"},
|
|
||||||
{file = "fonttools-4.41.1-cp39-cp39-win32.whl", hash = "sha256:afce2aeb80be72b4da7dd114f10f04873ff512793d13ce0b19d12b2a4c44c0f0"},
|
|
||||||
{file = "fonttools-4.41.1-cp39-cp39-win_amd64.whl", hash = "sha256:1df1b6f4c7c4bc8201eb47f3b268adbf2539943aa43c400f84556557e3e109c0"},
|
|
||||||
{file = "fonttools-4.41.1-py3-none-any.whl", hash = "sha256:952cb405f78734cf6466252fec42e206450d1a6715746013f64df9cbd4f896fa"},
|
|
||||||
{file = "fonttools-4.41.1.tar.gz", hash = "sha256:e16a9449f21a93909c5be2f5ed5246420f2316e94195dbfccb5238aaa38f9751"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0,<5)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.0.0)", "xattr", "zopfli (>=0.1.4)"]
|
|
||||||
graphite = ["lz4 (>=1.7.4.2)"]
|
|
||||||
interpolatable = ["munkres", "scipy"]
|
|
||||||
lxml = ["lxml (>=4.0,<5)"]
|
|
||||||
pathops = ["skia-pathops (>=0.5.0)"]
|
|
||||||
plot = ["matplotlib"]
|
|
||||||
repacker = ["uharfbuzz (>=0.23.0)"]
|
|
||||||
symfont = ["sympy"]
|
|
||||||
type1 = ["xattr"]
|
|
||||||
ufo = ["fs (>=2.2.0,<3)"]
|
|
||||||
unicode = ["unicodedata2 (>=15.0.0)"]
|
|
||||||
woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "freetype-py"
|
|
||||||
version = "2.4.0"
|
|
||||||
description = "Freetype python bindings"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
files = [
|
|
||||||
{file = "freetype-py-2.4.0.zip", hash = "sha256:8ad81195d2f8f339aba61700cebfbd77defad149c51f59b75a2a5e37833ae12e"},
|
|
||||||
{file = "freetype_py-2.4.0-py3-none-macosx_10_9_universal2.whl", hash = "sha256:3e0f5a91bc812f42d98a92137e86bac4ed037a29e43dafdb76d716d5732189e8"},
|
|
||||||
{file = "freetype_py-2.4.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9a3abc277f5f6d21575c0093c0c6139c161bf05b91aa6258505ab27c5001c5e"},
|
|
||||||
{file = "freetype_py-2.4.0-py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ce931f581d5038c4fea1f3d314254e0264e92441a5fdaef6817fe77b7bb888d3"},
|
|
||||||
{file = "freetype_py-2.4.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:c6276d92ac401c8ce02ea391fc854de413b01a8d835fb394ee5eb6f04fc947f5"},
|
|
||||||
{file = "freetype_py-2.4.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:9614f68876e9c62e821dfa811dd6160e00279d9d98cf60118cb264be48da1472"},
|
|
||||||
{file = "freetype_py-2.4.0-py3-none-win_amd64.whl", hash = "sha256:a2620788d4f0c00bd75fee2dfca61635ab0da856131598c96e2355d5257f70e5"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "idna"
|
|
||||||
version = "3.4"
|
|
||||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.5"
|
|
||||||
files = [
|
|
||||||
{file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
|
|
||||||
{file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "kiwisolver"
|
|
||||||
version = "1.4.4"
|
|
||||||
description = "A fast implementation of the Cassowary constraint solver"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
files = [
|
|
||||||
{file = "kiwisolver-1.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2f5e60fabb7343a836360c4f0919b8cd0d6dbf08ad2ca6b9cf90bf0c76a3c4f6"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:10ee06759482c78bdb864f4109886dff7b8a56529bc1609d4f1112b93fe6423c"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c79ebe8f3676a4c6630fd3f777f3cfecf9289666c84e775a67d1d358578dc2e3"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:abbe9fa13da955feb8202e215c4018f4bb57469b1b78c7a4c5c7b93001699938"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7577c1987baa3adc4b3c62c33bd1118c3ef5c8ddef36f0f2c950ae0b199e100d"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8ad8285b01b0d4695102546b342b493b3ccc6781fc28c8c6a1bb63e95d22f09"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ed58b8acf29798b036d347791141767ccf65eee7f26bde03a71c944449e53de"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a68b62a02953b9841730db7797422f983935aeefceb1679f0fc85cbfbd311c32"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp310-cp310-win32.whl", hash = "sha256:e92a513161077b53447160b9bd8f522edfbed4bd9759e4c18ab05d7ef7e49408"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:3fe20f63c9ecee44560d0e7f116b3a747a5d7203376abeea292ab3152334d004"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e0ea21f66820452a3f5d1655f8704a60d66ba1191359b96541eaf457710a5fc6"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bc9db8a3efb3e403e4ecc6cd9489ea2bac94244f80c78e27c31dcc00d2790ac2"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d5b61785a9ce44e5a4b880272baa7cf6c8f48a5180c3e81c59553ba0cb0821ca"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2dbb44c3f7e6c4d3487b31037b1bdbf424d97687c1747ce4ff2895795c9bf69"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6295ecd49304dcf3bfbfa45d9a081c96509e95f4b9d0eb7ee4ec0530c4a96514"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bd472dbe5e136f96a4b18f295d159d7f26fd399136f5b17b08c4e5f498cd494"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf7d9fce9bcc4752ca4a1b80aabd38f6d19009ea5cbda0e0856983cf6d0023f5"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d6601aed50c74e0ef02f4204da1816147a6d3fbdc8b3872d263338a9052c51"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:877272cf6b4b7e94c9614f9b10140e198d2186363728ed0f701c6eee1baec1da"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:db608a6757adabb32f1cfe6066e39b3706d8c3aa69bbc353a5b61edad36a5cb4"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5853eb494c71e267912275e5586fe281444eb5e722de4e131cddf9d442615626"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:f0a1dbdb5ecbef0d34eb77e56fcb3e95bbd7e50835d9782a45df81cc46949750"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:283dffbf061a4ec60391d51e6155e372a1f7a4f5b15d59c8505339454f8989e4"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp311-cp311-win32.whl", hash = "sha256:d06adcfa62a4431d404c31216f0f8ac97397d799cd53800e9d3efc2fbb3cf14e"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:e7da3fec7408813a7cebc9e4ec55afed2d0fd65c4754bc376bf03498d4e92686"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:62ac9cc684da4cf1778d07a89bf5f81b35834cb96ca523d3a7fb32509380cbf6"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41dae968a94b1ef1897cb322b39360a0812661dba7c682aa45098eb8e193dbdf"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02f79693ec433cb4b5f51694e8477ae83b3205768a6fb48ffba60549080e295b"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d0611a0a2a518464c05ddd5a3a1a0e856ccc10e67079bb17f265ad19ab3c7597"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:db5283d90da4174865d520e7366801a93777201e91e79bacbac6e6927cbceede"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1041feb4cda8708ce73bb4dcb9ce1ccf49d553bf87c3954bdfa46f0c3f77252c"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp37-cp37m-win32.whl", hash = "sha256:a553dadda40fef6bfa1456dc4be49b113aa92c2a9a9e8711e955618cd69622e3"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:03baab2d6b4a54ddbb43bba1a3a2d1627e82d205c5cf8f4c924dc49284b87166"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:841293b17ad704d70c578f1f0013c890e219952169ce8a24ebc063eecf775454"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f4f270de01dd3e129a72efad823da90cc4d6aafb64c410c9033aba70db9f1ff0"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f9f39e2f049db33a908319cf46624a569b36983c7c78318e9726a4cb8923b26c"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c97528e64cb9ebeff9701e7938653a9951922f2a38bd847787d4a8e498cc83ae"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d1573129aa0fd901076e2bfb4275a35f5b7aa60fbfb984499d661ec950320b0"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ad881edc7ccb9d65b0224f4e4d05a1e85cf62d73aab798943df6d48ab0cd79a1"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b428ef021242344340460fa4c9185d0b1f66fbdbfecc6c63eff4b7c29fad429d"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2e407cb4bd5a13984a6c2c0fe1845e4e41e96f183e5e5cd4d77a857d9693494c"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp38-cp38-win32.whl", hash = "sha256:75facbe9606748f43428fc91a43edb46c7ff68889b91fa31f53b58894503a191"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:5bce61af018b0cb2055e0e72e7d65290d822d3feee430b7b8203d8a855e78766"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8c808594c88a025d4e322d5bb549282c93c8e1ba71b790f539567932722d7bd8"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0a71d85ecdd570ded8ac3d1c0f480842f49a40beb423bb8014539a9f32a5897"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b533558eae785e33e8c148a8d9921692a9fe5aa516efbdff8606e7d87b9d5824"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:efda5fc8cc1c61e4f639b8067d118e742b812c930f708e6667a5ce0d13499e29"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7c43e1e1206cd421cd92e6b3280d4385d41d7166b3ed577ac20444b6995a445f"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc8d3bd6c72b2dd9decf16ce70e20abcb3274ba01b4e1c96031e0c4067d1e7cd"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ea39b0ccc4f5d803e3337dd46bcce60b702be4d86fd0b3d7531ef10fd99a1ac"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:968f44fdbf6dd757d12920d63b566eeb4d5b395fd2d00d29d7ef00a00582aac9"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp39-cp39-win32.whl", hash = "sha256:da7e547706e69e45d95e116e6939488d62174e033b763ab1496b4c29b76fabea"},
|
|
||||||
{file = "kiwisolver-1.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:ba59c92039ec0a66103b1d5fe588fa546373587a7d68f5c96f743c3396afc04b"},
|
|
||||||
{file = "kiwisolver-1.4.4-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:91672bacaa030f92fc2f43b620d7b337fd9a5af28b0d6ed3f77afc43c4a64b5a"},
|
|
||||||
{file = "kiwisolver-1.4.4-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:787518a6789009c159453da4d6b683f468ef7a65bbde796bcea803ccf191058d"},
|
|
||||||
{file = "kiwisolver-1.4.4-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da152d8cdcab0e56e4f45eb08b9aea6455845ec83172092f09b0e077ece2cf7a"},
|
|
||||||
{file = "kiwisolver-1.4.4-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ecb1fa0db7bf4cff9dac752abb19505a233c7f16684c5826d1f11ebd9472b871"},
|
|
||||||
{file = "kiwisolver-1.4.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:28bc5b299f48150b5f822ce68624e445040595a4ac3d59251703779836eceff9"},
|
|
||||||
{file = "kiwisolver-1.4.4-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:81e38381b782cc7e1e46c4e14cd997ee6040768101aefc8fa3c24a4cc58e98f8"},
|
|
||||||
{file = "kiwisolver-1.4.4-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2a66fdfb34e05b705620dd567f5a03f239a088d5a3f321e7b6ac3239d22aa286"},
|
|
||||||
{file = "kiwisolver-1.4.4-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:872b8ca05c40d309ed13eb2e582cab0c5a05e81e987ab9c521bf05ad1d5cf5cb"},
|
|
||||||
{file = "kiwisolver-1.4.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:70e7c2e7b750585569564e2e5ca9845acfaa5da56ac46df68414f29fea97be9f"},
|
|
||||||
{file = "kiwisolver-1.4.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9f85003f5dfa867e86d53fac6f7e6f30c045673fa27b603c397753bebadc3008"},
|
|
||||||
{file = "kiwisolver-1.4.4-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e307eb9bd99801f82789b44bb45e9f541961831c7311521b13a6c85afc09767"},
|
|
||||||
{file = "kiwisolver-1.4.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1792d939ec70abe76f5054d3f36ed5656021dcad1322d1cc996d4e54165cef9"},
|
|
||||||
{file = "kiwisolver-1.4.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6cb459eea32a4e2cf18ba5fcece2dbdf496384413bc1bae15583f19e567f3b2"},
|
|
||||||
{file = "kiwisolver-1.4.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:36dafec3d6d6088d34e2de6b85f9d8e2324eb734162fba59d2ba9ed7a2043d5b"},
|
|
||||||
{file = "kiwisolver-1.4.4.tar.gz", hash = "sha256:d41997519fcba4a1e46eb4a2fe31bc12f0ff957b2b81bac28db24744f333e955"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "latex2mathml"
|
|
||||||
version = "3.76.0"
|
|
||||||
description = "Pure Python library for LaTeX to MathML conversion"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8.1,<4.0.0"
|
|
||||||
files = [
|
|
||||||
{file = "latex2mathml-3.76.0-py3-none-any.whl", hash = "sha256:ac6d1d6bf5673d95acc82ebbb86b4d04a8a0737fd8664e8a28a0d28091b7b445"},
|
|
||||||
{file = "latex2mathml-3.76.0.tar.gz", hash = "sha256:b3ebae966ba2b578b4284738f44960791c204601271b43ddc4df5dd9d02cab0b"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lxml"
|
|
||||||
version = "4.9.3"
|
|
||||||
description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*"
|
|
||||||
files = [
|
|
||||||
{file = "lxml-4.9.3-cp27-cp27m-macosx_11_0_x86_64.whl", hash = "sha256:b0a545b46b526d418eb91754565ba5b63b1c0b12f9bd2f808c852d9b4b2f9b5c"},
|
|
||||||
{file = "lxml-4.9.3-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:075b731ddd9e7f68ad24c635374211376aa05a281673ede86cbe1d1b3455279d"},
|
|
||||||
{file = "lxml-4.9.3-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1e224d5755dba2f4a9498e150c43792392ac9b5380aa1b845f98a1618c94eeef"},
|
|
||||||
{file = "lxml-4.9.3-cp27-cp27m-win32.whl", hash = "sha256:2c74524e179f2ad6d2a4f7caf70e2d96639c0954c943ad601a9e146c76408ed7"},
|
|
||||||
{file = "lxml-4.9.3-cp27-cp27m-win_amd64.whl", hash = "sha256:4f1026bc732b6a7f96369f7bfe1a4f2290fb34dce00d8644bc3036fb351a4ca1"},
|
|
||||||
{file = "lxml-4.9.3-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0781a98ff5e6586926293e59480b64ddd46282953203c76ae15dbbbf302e8bb"},
|
|
||||||
{file = "lxml-4.9.3-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cef2502e7e8a96fe5ad686d60b49e1ab03e438bd9123987994528febd569868e"},
|
|
||||||
{file = "lxml-4.9.3-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:b86164d2cff4d3aaa1f04a14685cbc072efd0b4f99ca5708b2ad1b9b5988a991"},
|
|
||||||
{file = "lxml-4.9.3-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:42871176e7896d5d45138f6d28751053c711ed4d48d8e30b498da155af39aebd"},
|
|
||||||
{file = "lxml-4.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ae8b9c6deb1e634ba4f1930eb67ef6e6bf6a44b6eb5ad605642b2d6d5ed9ce3c"},
|
|
||||||
{file = "lxml-4.9.3-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:411007c0d88188d9f621b11d252cce90c4a2d1a49db6c068e3c16422f306eab8"},
|
|
||||||
{file = "lxml-4.9.3-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:cd47b4a0d41d2afa3e58e5bf1f62069255aa2fd6ff5ee41604418ca925911d76"},
|
|
||||||
{file = "lxml-4.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0e2cb47860da1f7e9a5256254b74ae331687b9672dfa780eed355c4c9c3dbd23"},
|
|
||||||
{file = "lxml-4.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1247694b26342a7bf47c02e513d32225ededd18045264d40758abeb3c838a51f"},
|
|
||||||
{file = "lxml-4.9.3-cp310-cp310-win32.whl", hash = "sha256:cdb650fc86227eba20de1a29d4b2c1bfe139dc75a0669270033cb2ea3d391b85"},
|
|
||||||
{file = "lxml-4.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:97047f0d25cd4bcae81f9ec9dc290ca3e15927c192df17331b53bebe0e3ff96d"},
|
|
||||||
{file = "lxml-4.9.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:1f447ea5429b54f9582d4b955f5f1985f278ce5cf169f72eea8afd9502973dd5"},
|
|
||||||
{file = "lxml-4.9.3-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:57d6ba0ca2b0c462f339640d22882acc711de224d769edf29962b09f77129cbf"},
|
|
||||||
{file = "lxml-4.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:9767e79108424fb6c3edf8f81e6730666a50feb01a328f4a016464a5893f835a"},
|
|
||||||
{file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:71c52db65e4b56b8ddc5bb89fb2e66c558ed9d1a74a45ceb7dcb20c191c3df2f"},
|
|
||||||
{file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d73d8ecf8ecf10a3bd007f2192725a34bd62898e8da27eb9d32a58084f93962b"},
|
|
||||||
{file = "lxml-4.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0a3d3487f07c1d7f150894c238299934a2a074ef590b583103a45002035be120"},
|
|
||||||
{file = "lxml-4.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e28c51fa0ce5674be9f560c6761c1b441631901993f76700b1b30ca6c8378d6"},
|
|
||||||
{file = "lxml-4.9.3-cp311-cp311-win32.whl", hash = "sha256:0bfd0767c5c1de2551a120673b72e5d4b628737cb05414f03c3277bf9bed3305"},
|
|
||||||
{file = "lxml-4.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:25f32acefac14ef7bd53e4218fe93b804ef6f6b92ffdb4322bb6d49d94cad2bc"},
|
|
||||||
{file = "lxml-4.9.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:d3ff32724f98fbbbfa9f49d82852b159e9784d6094983d9a8b7f2ddaebb063d4"},
|
|
||||||
{file = "lxml-4.9.3-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:48d6ed886b343d11493129e019da91d4039826794a3e3027321c56d9e71505be"},
|
|
||||||
{file = "lxml-4.9.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9a92d3faef50658dd2c5470af249985782bf754c4e18e15afb67d3ab06233f13"},
|
|
||||||
{file = "lxml-4.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b4e4bc18382088514ebde9328da057775055940a1f2e18f6ad2d78aa0f3ec5b9"},
|
|
||||||
{file = "lxml-4.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fc9b106a1bf918db68619fdcd6d5ad4f972fdd19c01d19bdb6bf63f3589a9ec5"},
|
|
||||||
{file = "lxml-4.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:d37017287a7adb6ab77e1c5bee9bcf9660f90ff445042b790402a654d2ad81d8"},
|
|
||||||
{file = "lxml-4.9.3-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:56dc1f1ebccc656d1b3ed288f11e27172a01503fc016bcabdcbc0978b19352b7"},
|
|
||||||
{file = "lxml-4.9.3-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:578695735c5a3f51569810dfebd05dd6f888147a34f0f98d4bb27e92b76e05c2"},
|
|
||||||
{file = "lxml-4.9.3-cp35-cp35m-win32.whl", hash = "sha256:704f61ba8c1283c71b16135caf697557f5ecf3e74d9e453233e4771d68a1f42d"},
|
|
||||||
{file = "lxml-4.9.3-cp35-cp35m-win_amd64.whl", hash = "sha256:c41bfca0bd3532d53d16fd34d20806d5c2b1ace22a2f2e4c0008570bf2c58833"},
|
|
||||||
{file = "lxml-4.9.3-cp36-cp36m-macosx_11_0_x86_64.whl", hash = "sha256:64f479d719dc9f4c813ad9bb6b28f8390360660b73b2e4beb4cb0ae7104f1c12"},
|
|
||||||
{file = "lxml-4.9.3-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:dd708cf4ee4408cf46a48b108fb9427bfa00b9b85812a9262b5c668af2533ea5"},
|
|
||||||
{file = "lxml-4.9.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c31c7462abdf8f2ac0577d9f05279727e698f97ecbb02f17939ea99ae8daa98"},
|
|
||||||
{file = "lxml-4.9.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e3cd95e10c2610c360154afdc2f1480aea394f4a4f1ea0a5eacce49640c9b190"},
|
|
||||||
{file = "lxml-4.9.3-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:4930be26af26ac545c3dffb662521d4e6268352866956672231887d18f0eaab2"},
|
|
||||||
{file = "lxml-4.9.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4aec80cde9197340bc353d2768e2a75f5f60bacda2bab72ab1dc499589b3878c"},
|
|
||||||
{file = "lxml-4.9.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:14e019fd83b831b2e61baed40cab76222139926b1fb5ed0e79225bc0cae14584"},
|
|
||||||
{file = "lxml-4.9.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0c0850c8b02c298d3c7006b23e98249515ac57430e16a166873fc47a5d549287"},
|
|
||||||
{file = "lxml-4.9.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:aca086dc5f9ef98c512bac8efea4483eb84abbf926eaeedf7b91479feb092458"},
|
|
||||||
{file = "lxml-4.9.3-cp36-cp36m-win32.whl", hash = "sha256:50baa9c1c47efcaef189f31e3d00d697c6d4afda5c3cde0302d063492ff9b477"},
|
|
||||||
{file = "lxml-4.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bef4e656f7d98aaa3486d2627e7d2df1157d7e88e7efd43a65aa5dd4714916cf"},
|
|
||||||
{file = "lxml-4.9.3-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:46f409a2d60f634fe550f7133ed30ad5321ae2e6630f13657fb9479506b00601"},
|
|
||||||
{file = "lxml-4.9.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:4c28a9144688aef80d6ea666c809b4b0e50010a2aca784c97f5e6bf143d9f129"},
|
|
||||||
{file = "lxml-4.9.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:141f1d1a9b663c679dc524af3ea1773e618907e96075262726c7612c02b149a4"},
|
|
||||||
{file = "lxml-4.9.3-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:53ace1c1fd5a74ef662f844a0413446c0629d151055340e9893da958a374f70d"},
|
|
||||||
{file = "lxml-4.9.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:17a753023436a18e27dd7769e798ce302963c236bc4114ceee5b25c18c52c693"},
|
|
||||||
{file = "lxml-4.9.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7d298a1bd60c067ea75d9f684f5f3992c9d6766fadbc0bcedd39750bf344c2f4"},
|
|
||||||
{file = "lxml-4.9.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:081d32421db5df44c41b7f08a334a090a545c54ba977e47fd7cc2deece78809a"},
|
|
||||||
{file = "lxml-4.9.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:23eed6d7b1a3336ad92d8e39d4bfe09073c31bfe502f20ca5116b2a334f8ec02"},
|
|
||||||
{file = "lxml-4.9.3-cp37-cp37m-win32.whl", hash = "sha256:1509dd12b773c02acd154582088820893109f6ca27ef7291b003d0e81666109f"},
|
|
||||||
{file = "lxml-4.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:120fa9349a24c7043854c53cae8cec227e1f79195a7493e09e0c12e29f918e52"},
|
|
||||||
{file = "lxml-4.9.3-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4d2d1edbca80b510443f51afd8496be95529db04a509bc8faee49c7b0fb6d2cc"},
|
|
||||||
{file = "lxml-4.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8d7e43bd40f65f7d97ad8ef5c9b1778943d02f04febef12def25f7583d19baac"},
|
|
||||||
{file = "lxml-4.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:71d66ee82e7417828af6ecd7db817913cb0cf9d4e61aa0ac1fde0583d84358db"},
|
|
||||||
{file = "lxml-4.9.3-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:6fc3c450eaa0b56f815c7b62f2b7fba7266c4779adcf1cece9e6deb1de7305ce"},
|
|
||||||
{file = "lxml-4.9.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65299ea57d82fb91c7f019300d24050c4ddeb7c5a190e076b5f48a2b43d19c42"},
|
|
||||||
{file = "lxml-4.9.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:eadfbbbfb41b44034a4c757fd5d70baccd43296fb894dba0295606a7cf3124aa"},
|
|
||||||
{file = "lxml-4.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3e9bdd30efde2b9ccfa9cb5768ba04fe71b018a25ea093379c857c9dad262c40"},
|
|
||||||
{file = "lxml-4.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fcdd00edfd0a3001e0181eab3e63bd5c74ad3e67152c84f93f13769a40e073a7"},
|
|
||||||
{file = "lxml-4.9.3-cp38-cp38-win32.whl", hash = "sha256:57aba1bbdf450b726d58b2aea5fe47c7875f5afb2c4a23784ed78f19a0462574"},
|
|
||||||
{file = "lxml-4.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:92af161ecbdb2883c4593d5ed4815ea71b31fafd7fd05789b23100d081ecac96"},
|
|
||||||
{file = "lxml-4.9.3-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:9bb6ad405121241e99a86efff22d3ef469024ce22875a7ae045896ad23ba2340"},
|
|
||||||
{file = "lxml-4.9.3-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8ed74706b26ad100433da4b9d807eae371efaa266ffc3e9191ea436087a9d6a7"},
|
|
||||||
{file = "lxml-4.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fbf521479bcac1e25a663df882c46a641a9bff6b56dc8b0fafaebd2f66fb231b"},
|
|
||||||
{file = "lxml-4.9.3-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:303bf1edce6ced16bf67a18a1cf8339d0db79577eec5d9a6d4a80f0fb10aa2da"},
|
|
||||||
{file = "lxml-4.9.3-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:5515edd2a6d1a5a70bfcdee23b42ec33425e405c5b351478ab7dc9347228f96e"},
|
|
||||||
{file = "lxml-4.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:690dafd0b187ed38583a648076865d8c229661ed20e48f2335d68e2cf7dc829d"},
|
|
||||||
{file = "lxml-4.9.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6420a005548ad52154c8ceab4a1290ff78d757f9e5cbc68f8c77089acd3c432"},
|
|
||||||
{file = "lxml-4.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bb3bb49c7a6ad9d981d734ef7c7193bc349ac338776a0360cc671eaee89bcf69"},
|
|
||||||
{file = "lxml-4.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d27be7405547d1f958b60837dc4c1007da90b8b23f54ba1f8b728c78fdb19d50"},
|
|
||||||
{file = "lxml-4.9.3-cp39-cp39-win32.whl", hash = "sha256:8df133a2ea5e74eef5e8fc6f19b9e085f758768a16e9877a60aec455ed2609b2"},
|
|
||||||
{file = "lxml-4.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:4dd9a263e845a72eacb60d12401e37c616438ea2e5442885f65082c276dfb2b2"},
|
|
||||||
{file = "lxml-4.9.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6689a3d7fd13dc687e9102a27e98ef33730ac4fe37795d5036d18b4d527abd35"},
|
|
||||||
{file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:f6bdac493b949141b733c5345b6ba8f87a226029cbabc7e9e121a413e49441e0"},
|
|
||||||
{file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:05186a0f1346ae12553d66df1cfce6f251589fea3ad3da4f3ef4e34b2d58c6a3"},
|
|
||||||
{file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c2006f5c8d28dee289f7020f721354362fa304acbaaf9745751ac4006650254b"},
|
|
||||||
{file = "lxml-4.9.3-pp38-pypy38_pp73-macosx_11_0_x86_64.whl", hash = "sha256:5c245b783db29c4e4fbbbfc9c5a78be496c9fea25517f90606aa1f6b2b3d5f7b"},
|
|
||||||
{file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4fb960a632a49f2f089d522f70496640fdf1218f1243889da3822e0a9f5f3ba7"},
|
|
||||||
{file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:50670615eaf97227d5dc60de2dc99fb134a7130d310d783314e7724bf163f75d"},
|
|
||||||
{file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9719fe17307a9e814580af1f5c6e05ca593b12fb7e44fe62450a5384dbf61b4b"},
|
|
||||||
{file = "lxml-4.9.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:3331bece23c9ee066e0fb3f96c61322b9e0f54d775fccefff4c38ca488de283a"},
|
|
||||||
{file = "lxml-4.9.3-pp39-pypy39_pp73-macosx_11_0_x86_64.whl", hash = "sha256:ed667f49b11360951e201453fc3967344d0d0263aa415e1619e85ae7fd17b4e0"},
|
|
||||||
{file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8b77946fd508cbf0fccd8e400a7f71d4ac0e1595812e66025bac475a8e811694"},
|
|
||||||
{file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e4da8ca0c0c0aea88fd46be8e44bd49716772358d648cce45fe387f7b92374a7"},
|
|
||||||
{file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fe4bda6bd4340caa6e5cf95e73f8fea5c4bfc55763dd42f1b50a94c1b4a2fbd4"},
|
|
||||||
{file = "lxml-4.9.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f3df3db1d336b9356dd3112eae5f5c2b8b377f3bc826848567f10bfddfee77e9"},
|
|
||||||
{file = "lxml-4.9.3.tar.gz", hash = "sha256:48628bd53a426c9eb9bc066a923acaa0878d1e86129fd5359aee99285f4eed9c"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
cssselect = ["cssselect (>=0.7)"]
|
|
||||||
html5 = ["html5lib"]
|
|
||||||
htmlsoup = ["BeautifulSoup4"]
|
|
||||||
source = ["Cython (>=0.29.35)"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "marko"
|
|
||||||
version = "2.0.0"
|
|
||||||
description = "A markdown parser with high extensibility."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
files = [
|
|
||||||
{file = "marko-2.0.0-py3-none-any.whl", hash = "sha256:d5a8221e0d4a9369e662c33ccfe33dea7deb2bebcf1ac3d5a4b46d42ab309e8f"},
|
|
||||||
{file = "marko-2.0.0.tar.gz", hash = "sha256:efc26460893250c13751025ae9202ebb1389887036fc0df9009c6ab875467030"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
codehilite = ["pygments"]
|
|
||||||
toc = ["python-slugify"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "matplotlib"
|
|
||||||
version = "3.7.2"
|
|
||||||
description = "Python plotting package"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "matplotlib-3.7.2-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:2699f7e73a76d4c110f4f25be9d2496d6ab4f17345307738557d345f099e07de"},
|
|
||||||
{file = "matplotlib-3.7.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a8035ba590658bae7562786c9cc6ea1a84aa49d3afab157e414c9e2ea74f496d"},
|
|
||||||
{file = "matplotlib-3.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2f8e4a49493add46ad4a8c92f63e19d548b2b6ebbed75c6b4c7f46f57d36cdd1"},
|
|
||||||
{file = "matplotlib-3.7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71667eb2ccca4c3537d9414b1bc00554cb7f91527c17ee4ec38027201f8f1603"},
|
|
||||||
{file = "matplotlib-3.7.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:152ee0b569a37630d8628534c628456b28686e085d51394da6b71ef84c4da201"},
|
|
||||||
{file = "matplotlib-3.7.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:070f8dddd1f5939e60aacb8fa08f19551f4b0140fab16a3669d5cd6e9cb28fc8"},
|
|
||||||
{file = "matplotlib-3.7.2-cp310-cp310-win32.whl", hash = "sha256:fdbb46fad4fb47443b5b8ac76904b2e7a66556844f33370861b4788db0f8816a"},
|
|
||||||
{file = "matplotlib-3.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:23fb1750934e5f0128f9423db27c474aa32534cec21f7b2153262b066a581fd1"},
|
|
||||||
{file = "matplotlib-3.7.2-cp311-cp311-macosx_10_12_universal2.whl", hash = "sha256:30e1409b857aa8a747c5d4f85f63a79e479835f8dffc52992ac1f3f25837b544"},
|
|
||||||
{file = "matplotlib-3.7.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:50e0a55ec74bf2d7a0ebf50ac580a209582c2dd0f7ab51bc270f1b4a0027454e"},
|
|
||||||
{file = "matplotlib-3.7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ac60daa1dc83e8821eed155796b0f7888b6b916cf61d620a4ddd8200ac70cd64"},
|
|
||||||
{file = "matplotlib-3.7.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:305e3da477dc8607336ba10bac96986d6308d614706cae2efe7d3ffa60465b24"},
|
|
||||||
{file = "matplotlib-3.7.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c308b255efb9b06b23874236ec0f10f026673ad6515f602027cc8ac7805352d"},
|
|
||||||
{file = "matplotlib-3.7.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60c521e21031632aa0d87ca5ba0c1c05f3daacadb34c093585a0be6780f698e4"},
|
|
||||||
{file = "matplotlib-3.7.2-cp311-cp311-win32.whl", hash = "sha256:26bede320d77e469fdf1bde212de0ec889169b04f7f1179b8930d66f82b30cbc"},
|
|
||||||
{file = "matplotlib-3.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:af4860132c8c05261a5f5f8467f1b269bf1c7c23902d75f2be57c4a7f2394b3e"},
|
|
||||||
{file = "matplotlib-3.7.2-cp38-cp38-macosx_10_12_universal2.whl", hash = "sha256:a1733b8e84e7e40a9853e505fe68cc54339f97273bdfe6f3ed980095f769ddc7"},
|
|
||||||
{file = "matplotlib-3.7.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d9881356dc48e58910c53af82b57183879129fa30492be69058c5b0d9fddf391"},
|
|
||||||
{file = "matplotlib-3.7.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f081c03f413f59390a80b3e351cc2b2ea0205839714dbc364519bcf51f4b56ca"},
|
|
||||||
{file = "matplotlib-3.7.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1cd120fca3407a225168238b790bd5c528f0fafde6172b140a2f3ab7a4ea63e9"},
|
|
||||||
{file = "matplotlib-3.7.2-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a2c1590b90aa7bd741b54c62b78de05d4186271e34e2377e0289d943b3522273"},
|
|
||||||
{file = "matplotlib-3.7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d2ff3c984b8a569bc1383cd468fc06b70d7b59d5c2854ca39f1436ae8394117"},
|
|
||||||
{file = "matplotlib-3.7.2-cp38-cp38-win32.whl", hash = "sha256:5dea00b62d28654b71ca92463656d80646675628d0828e08a5f3b57e12869e13"},
|
|
||||||
{file = "matplotlib-3.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:0f506a1776ee94f9e131af1ac6efa6e5bc7cb606a3e389b0ccb6e657f60bb676"},
|
|
||||||
{file = "matplotlib-3.7.2-cp39-cp39-macosx_10_12_universal2.whl", hash = "sha256:6515e878f91894c2e4340d81f0911857998ccaf04dbc1bba781e3d89cbf70608"},
|
|
||||||
{file = "matplotlib-3.7.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:71f7a8c6b124e904db550f5b9fe483d28b896d4135e45c4ea381ad3b8a0e3256"},
|
|
||||||
{file = "matplotlib-3.7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:12f01b92ecd518e0697da4d97d163b2b3aa55eb3eb4e2c98235b3396d7dad55f"},
|
|
||||||
{file = "matplotlib-3.7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7e28d6396563955f7af437894a36bf2b279462239a41028323e04b85179058b"},
|
|
||||||
{file = "matplotlib-3.7.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbcf59334ff645e6a67cd5f78b4b2cdb76384cdf587fa0d2dc85f634a72e1a3e"},
|
|
||||||
{file = "matplotlib-3.7.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:318c89edde72ff95d8df67d82aca03861240512994a597a435a1011ba18dbc7f"},
|
|
||||||
{file = "matplotlib-3.7.2-cp39-cp39-win32.whl", hash = "sha256:ce55289d5659b5b12b3db4dc9b7075b70cef5631e56530f14b2945e8836f2d20"},
|
|
||||||
{file = "matplotlib-3.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:2ecb5be2b2815431c81dc115667e33da0f5a1bcf6143980d180d09a717c4a12e"},
|
|
||||||
{file = "matplotlib-3.7.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:fdcd28360dbb6203fb5219b1a5658df226ac9bebc2542a9e8f457de959d713d0"},
|
|
||||||
{file = "matplotlib-3.7.2-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c3cca3e842b11b55b52c6fb8bd6a4088693829acbfcdb3e815fa9b7d5c92c1b"},
|
|
||||||
{file = "matplotlib-3.7.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebf577c7a6744e9e1bd3fee45fc74a02710b214f94e2bde344912d85e0c9af7c"},
|
|
||||||
{file = "matplotlib-3.7.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:936bba394682049919dda062d33435b3be211dc3dcaa011e09634f060ec878b2"},
|
|
||||||
{file = "matplotlib-3.7.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bc221ffbc2150458b1cd71cdd9ddd5bb37962b036e41b8be258280b5b01da1dd"},
|
|
||||||
{file = "matplotlib-3.7.2-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35d74ebdb3f71f112b36c2629cf32323adfbf42679e2751252acd468f5001c07"},
|
|
||||||
{file = "matplotlib-3.7.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:717157e61b3a71d3d26ad4e1770dc85156c9af435659a25ee6407dc866cb258d"},
|
|
||||||
{file = "matplotlib-3.7.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:20f844d6be031948148ba49605c8b96dfe7d3711d1b63592830d650622458c11"},
|
|
||||||
{file = "matplotlib-3.7.2.tar.gz", hash = "sha256:a8cdb91dddb04436bd2f098b8fdf4b81352e68cf4d2c6756fcc414791076569b"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
contourpy = ">=1.0.1"
|
|
||||||
cycler = ">=0.10"
|
|
||||||
fonttools = ">=4.22.0"
|
|
||||||
kiwisolver = ">=1.0.1"
|
|
||||||
numpy = ">=1.20"
|
|
||||||
packaging = ">=20.0"
|
|
||||||
pillow = ">=6.2.0"
|
|
||||||
pyparsing = ">=2.3.1,<3.1"
|
|
||||||
python-dateutil = ">=2.7"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "numpy"
|
|
||||||
version = "1.25.1"
|
|
||||||
description = "Fundamental package for array computing in Python"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.9"
|
|
||||||
files = [
|
|
||||||
{file = "numpy-1.25.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:77d339465dff3eb33c701430bcb9c325b60354698340229e1dff97745e6b3efa"},
|
|
||||||
{file = "numpy-1.25.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d736b75c3f2cb96843a5c7f8d8ccc414768d34b0a75f466c05f3a739b406f10b"},
|
|
||||||
{file = "numpy-1.25.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a90725800caeaa160732d6b31f3f843ebd45d6b5f3eec9e8cc287e30f2805bf"},
|
|
||||||
{file = "numpy-1.25.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c6c9261d21e617c6dc5eacba35cb68ec36bb72adcff0dee63f8fbc899362588"},
|
|
||||||
{file = "numpy-1.25.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0def91f8af6ec4bb94c370e38c575855bf1d0be8a8fbfba42ef9c073faf2cf19"},
|
|
||||||
{file = "numpy-1.25.1-cp310-cp310-win32.whl", hash = "sha256:fd67b306320dcadea700a8f79b9e671e607f8696e98ec255915c0c6d6b818503"},
|
|
||||||
{file = "numpy-1.25.1-cp310-cp310-win_amd64.whl", hash = "sha256:c1516db588987450b85595586605742879e50dcce923e8973f79529651545b57"},
|
|
||||||
{file = "numpy-1.25.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6b82655dd8efeea69dbf85d00fca40013d7f503212bc5259056244961268b66e"},
|
|
||||||
{file = "numpy-1.25.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e8f6049c4878cb16960fbbfb22105e49d13d752d4d8371b55110941fb3b17800"},
|
|
||||||
{file = "numpy-1.25.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41a56b70e8139884eccb2f733c2f7378af06c82304959e174f8e7370af112e09"},
|
|
||||||
{file = "numpy-1.25.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5154b1a25ec796b1aee12ac1b22f414f94752c5f94832f14d8d6c9ac40bcca6"},
|
|
||||||
{file = "numpy-1.25.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:38eb6548bb91c421261b4805dc44def9ca1a6eef6444ce35ad1669c0f1a3fc5d"},
|
|
||||||
{file = "numpy-1.25.1-cp311-cp311-win32.whl", hash = "sha256:791f409064d0a69dd20579345d852c59822c6aa087f23b07b1b4e28ff5880fcb"},
|
|
||||||
{file = "numpy-1.25.1-cp311-cp311-win_amd64.whl", hash = "sha256:c40571fe966393b212689aa17e32ed905924120737194b5d5c1b20b9ed0fb171"},
|
|
||||||
{file = "numpy-1.25.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3d7abcdd85aea3e6cdddb59af2350c7ab1ed764397f8eec97a038ad244d2d105"},
|
|
||||||
{file = "numpy-1.25.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1a180429394f81c7933634ae49b37b472d343cccb5bb0c4a575ac8bbc433722f"},
|
|
||||||
{file = "numpy-1.25.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d412c1697c3853c6fc3cb9751b4915859c7afe6a277c2bf00acf287d56c4e625"},
|
|
||||||
{file = "numpy-1.25.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20e1266411120a4f16fad8efa8e0454d21d00b8c7cee5b5ccad7565d95eb42dd"},
|
|
||||||
{file = "numpy-1.25.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f76aebc3358ade9eacf9bc2bb8ae589863a4f911611694103af05346637df1b7"},
|
|
||||||
{file = "numpy-1.25.1-cp39-cp39-win32.whl", hash = "sha256:247d3ffdd7775bdf191f848be8d49100495114c82c2bd134e8d5d075fb386a1c"},
|
|
||||||
{file = "numpy-1.25.1-cp39-cp39-win_amd64.whl", hash = "sha256:1d5d3c68e443c90b38fdf8ef40e60e2538a27548b39b12b73132456847f4b631"},
|
|
||||||
{file = "numpy-1.25.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:35a9527c977b924042170a0887de727cd84ff179e478481404c5dc66b4170009"},
|
|
||||||
{file = "numpy-1.25.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d3fe3dd0506a28493d82dc3cf254be8cd0d26f4008a417385cbf1ae95b54004"},
|
|
||||||
{file = "numpy-1.25.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:012097b5b0d00a11070e8f2e261128c44157a8689f7dedcf35576e525893f4fe"},
|
|
||||||
{file = "numpy-1.25.1.tar.gz", hash = "sha256:9a3a9f3a61480cc086117b426a8bd86869c213fc4072e606f01c4e4b66eb92bf"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "packaging"
|
|
||||||
version = "23.1"
|
|
||||||
description = "Core utilities for Python packages"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
files = [
|
|
||||||
{file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"},
|
|
||||||
{file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pillow"
|
|
||||||
version = "10.0.0"
|
|
||||||
description = "Python Imaging Library (Fork)"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "Pillow-10.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f62406a884ae75fb2f818694469519fb685cc7eaff05d3451a9ebe55c646891"},
|
|
||||||
{file = "Pillow-10.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d5db32e2a6ccbb3d34d87c87b432959e0db29755727afb37290e10f6e8e62614"},
|
|
||||||
{file = "Pillow-10.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edf4392b77bdc81f36e92d3a07a5cd072f90253197f4a52a55a8cec48a12483b"},
|
|
||||||
{file = "Pillow-10.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:520f2a520dc040512699f20fa1c363eed506e94248d71f85412b625026f6142c"},
|
|
||||||
{file = "Pillow-10.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:8c11160913e3dd06c8ffdb5f233a4f254cb449f4dfc0f8f4549eda9e542c93d1"},
|
|
||||||
{file = "Pillow-10.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a74ba0c356aaa3bb8e3eb79606a87669e7ec6444be352870623025d75a14a2bf"},
|
|
||||||
{file = "Pillow-10.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5d0dae4cfd56969d23d94dc8e89fb6a217be461c69090768227beb8ed28c0a3"},
|
|
||||||
{file = "Pillow-10.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22c10cc517668d44b211717fd9775799ccec4124b9a7f7b3635fc5386e584992"},
|
|
||||||
{file = "Pillow-10.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:dffe31a7f47b603318c609f378ebcd57f1554a3a6a8effbc59c3c69f804296de"},
|
|
||||||
{file = "Pillow-10.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:9fb218c8a12e51d7ead2a7c9e101a04982237d4855716af2e9499306728fb485"},
|
|
||||||
{file = "Pillow-10.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d35e3c8d9b1268cbf5d3670285feb3528f6680420eafe35cccc686b73c1e330f"},
|
|
||||||
{file = "Pillow-10.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ed64f9ca2f0a95411e88a4efbd7a29e5ce2cea36072c53dd9d26d9c76f753b3"},
|
|
||||||
{file = "Pillow-10.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b6eb5502f45a60a3f411c63187db83a3d3107887ad0d036c13ce836f8a36f1d"},
|
|
||||||
{file = "Pillow-10.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:c1fbe7621c167ecaa38ad29643d77a9ce7311583761abf7836e1510c580bf3dd"},
|
|
||||||
{file = "Pillow-10.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:cd25d2a9d2b36fcb318882481367956d2cf91329f6892fe5d385c346c0649629"},
|
|
||||||
{file = "Pillow-10.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3b08d4cc24f471b2c8ca24ec060abf4bebc6b144cb89cba638c720546b1cf538"},
|
|
||||||
{file = "Pillow-10.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d737a602fbd82afd892ca746392401b634e278cb65d55c4b7a8f48e9ef8d008d"},
|
|
||||||
{file = "Pillow-10.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:3a82c40d706d9aa9734289740ce26460a11aeec2d9c79b7af87bb35f0073c12f"},
|
|
||||||
{file = "Pillow-10.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:bc2ec7c7b5d66b8ec9ce9f720dbb5fa4bace0f545acd34870eff4a369b44bf37"},
|
|
||||||
{file = "Pillow-10.0.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:d80cf684b541685fccdd84c485b31ce73fc5c9b5d7523bf1394ce134a60c6883"},
|
|
||||||
{file = "Pillow-10.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76de421f9c326da8f43d690110f0e79fe3ad1e54be811545d7d91898b4c8493e"},
|
|
||||||
{file = "Pillow-10.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81ff539a12457809666fef6624684c008e00ff6bf455b4b89fd00a140eecd640"},
|
|
||||||
{file = "Pillow-10.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce543ed15570eedbb85df19b0a1a7314a9c8141a36ce089c0a894adbfccb4568"},
|
|
||||||
{file = "Pillow-10.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:685ac03cc4ed5ebc15ad5c23bc555d68a87777586d970c2c3e216619a5476223"},
|
|
||||||
{file = "Pillow-10.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d72e2ecc68a942e8cf9739619b7f408cc7b272b279b56b2c83c6123fcfa5cdff"},
|
|
||||||
{file = "Pillow-10.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d50b6aec14bc737742ca96e85d6d0a5f9bfbded018264b3b70ff9d8c33485551"},
|
|
||||||
{file = "Pillow-10.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:00e65f5e822decd501e374b0650146063fbb30a7264b4d2744bdd7b913e0cab5"},
|
|
||||||
{file = "Pillow-10.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:f31f9fdbfecb042d046f9d91270a0ba28368a723302786c0009ee9b9f1f60199"},
|
|
||||||
{file = "Pillow-10.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:1ce91b6ec08d866b14413d3f0bbdea7e24dfdc8e59f562bb77bc3fe60b6144ca"},
|
|
||||||
{file = "Pillow-10.0.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:349930d6e9c685c089284b013478d6f76e3a534e36ddfa912cde493f235372f3"},
|
|
||||||
{file = "Pillow-10.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3a684105f7c32488f7153905a4e3015a3b6c7182e106fe3c37fbb5ef3e6994c3"},
|
|
||||||
{file = "Pillow-10.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4f69b3700201b80bb82c3a97d5e9254084f6dd5fb5b16fc1a7b974260f89f43"},
|
|
||||||
{file = "Pillow-10.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f07ea8d2f827d7d2a49ecf1639ec02d75ffd1b88dcc5b3a61bbb37a8759ad8d"},
|
|
||||||
{file = "Pillow-10.0.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:040586f7d37b34547153fa383f7f9aed68b738992380ac911447bb78f2abe530"},
|
|
||||||
{file = "Pillow-10.0.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:f88a0b92277de8e3ca715a0d79d68dc82807457dae3ab8699c758f07c20b3c51"},
|
|
||||||
{file = "Pillow-10.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c7cf14a27b0d6adfaebb3ae4153f1e516df54e47e42dcc073d7b3d76111a8d86"},
|
|
||||||
{file = "Pillow-10.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3400aae60685b06bb96f99a21e1ada7bc7a413d5f49bce739828ecd9391bb8f7"},
|
|
||||||
{file = "Pillow-10.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:dbc02381779d412145331789b40cc7b11fdf449e5d94f6bc0b080db0a56ea3f0"},
|
|
||||||
{file = "Pillow-10.0.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:9211e7ad69d7c9401cfc0e23d49b69ca65ddd898976d660a2fa5904e3d7a9baa"},
|
|
||||||
{file = "Pillow-10.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:faaf07ea35355b01a35cb442dd950d8f1bb5b040a7787791a535de13db15ed90"},
|
|
||||||
{file = "Pillow-10.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9f72a021fbb792ce98306ffb0c348b3c9cb967dce0f12a49aa4c3d3fdefa967"},
|
|
||||||
{file = "Pillow-10.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f7c16705f44e0504a3a2a14197c1f0b32a95731d251777dcb060aa83022cb2d"},
|
|
||||||
{file = "Pillow-10.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:76edb0a1fa2b4745fb0c99fb9fb98f8b180a1bbceb8be49b087e0b21867e77d3"},
|
|
||||||
{file = "Pillow-10.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:368ab3dfb5f49e312231b6f27b8820c823652b7cd29cfbd34090565a015e99ba"},
|
|
||||||
{file = "Pillow-10.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:608bfdee0d57cf297d32bcbb3c728dc1da0907519d1784962c5f0c68bb93e5a3"},
|
|
||||||
{file = "Pillow-10.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5c6e3df6bdd396749bafd45314871b3d0af81ff935b2d188385e970052091017"},
|
|
||||||
{file = "Pillow-10.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:7be600823e4c8631b74e4a0d38384c73f680e6105a7d3c6824fcf226c178c7e6"},
|
|
||||||
{file = "Pillow-10.0.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:92be919bbc9f7d09f7ae343c38f5bb21c973d2576c1d45600fce4b74bafa7ac0"},
|
|
||||||
{file = "Pillow-10.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8182b523b2289f7c415f589118228d30ac8c355baa2f3194ced084dac2dbba"},
|
|
||||||
{file = "Pillow-10.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:38250a349b6b390ee6047a62c086d3817ac69022c127f8a5dc058c31ccef17f3"},
|
|
||||||
{file = "Pillow-10.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:88af2003543cc40c80f6fca01411892ec52b11021b3dc22ec3bc9d5afd1c5334"},
|
|
||||||
{file = "Pillow-10.0.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c189af0545965fa8d3b9613cfdb0cd37f9d71349e0f7750e1fd704648d475ed2"},
|
|
||||||
{file = "Pillow-10.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce7b031a6fc11365970e6a5686d7ba8c63e4c1cf1ea143811acbb524295eabed"},
|
|
||||||
{file = "Pillow-10.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:db24668940f82321e746773a4bc617bfac06ec831e5c88b643f91f122a785684"},
|
|
||||||
{file = "Pillow-10.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:efe8c0681042536e0d06c11f48cebe759707c9e9abf880ee213541c5b46c5bf3"},
|
|
||||||
{file = "Pillow-10.0.0.tar.gz", hash = "sha256:9c82b5b3e043c7af0d95792d0d20ccf68f61a1fec6b3530e718b688422727396"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"]
|
|
||||||
tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pygments"
|
|
||||||
version = "2.16.1"
|
|
||||||
description = "Pygments is a syntax highlighting package written in Python."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
files = [
|
|
||||||
{file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"},
|
|
||||||
{file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
plugins = ["importlib-metadata"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pyparsing"
|
|
||||||
version = "3.0.9"
|
|
||||||
description = "pyparsing module - Classes and methods to define and execute parsing grammars"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.6.8"
|
|
||||||
files = [
|
|
||||||
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
|
|
||||||
{file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
diagrams = ["jinja2", "railroad-diagrams"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "python-dateutil"
|
|
||||||
version = "2.8.2"
|
|
||||||
description = "Extensions to the standard Python datetime module"
|
|
||||||
optional = false
|
|
||||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
|
||||||
files = [
|
|
||||||
{file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
|
|
||||||
{file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
six = ">=1.5"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "python-docx"
|
|
||||||
version = "0.8.11"
|
|
||||||
description = "Create and update Microsoft Word .docx files."
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
files = [
|
|
||||||
{file = "python-docx-0.8.11.tar.gz", hash = "sha256:1105d233a0956dd8dd1e710d20b159e2d72ac3c301041b95f4d4ceb3e0ebebc4"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
lxml = ">=2.3.2"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "requests"
|
|
||||||
version = "2.31.0"
|
|
||||||
description = "Python HTTP for Humans."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
files = [
|
|
||||||
{file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"},
|
|
||||||
{file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
certifi = ">=2017.4.17"
|
|
||||||
charset-normalizer = ">=2,<4"
|
|
||||||
idna = ">=2.5,<4"
|
|
||||||
urllib3 = ">=1.21.1,<3"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
|
|
||||||
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "setuptools"
|
|
||||||
version = "68.0.0"
|
|
||||||
description = "Easily download, build, install, upgrade, and uninstall Python packages"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
files = [
|
|
||||||
{file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"},
|
|
||||||
{file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
|
|
||||||
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
|
|
||||||
testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "six"
|
|
||||||
version = "1.16.0"
|
|
||||||
description = "Python 2 and 3 compatibility utilities"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
|
||||||
files = [
|
|
||||||
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
|
||||||
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "urllib3"
|
|
||||||
version = "2.0.4"
|
|
||||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
files = [
|
|
||||||
{file = "urllib3-2.0.4-py3-none-any.whl", hash = "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4"},
|
|
||||||
{file = "urllib3-2.0.4.tar.gz", hash = "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
|
|
||||||
secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"]
|
|
||||||
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
|
|
||||||
zstd = ["zstandard (>=0.18.0)"]
|
|
||||||
|
|
||||||
[metadata]
|
|
||||||
lock-version = "2.0"
|
|
||||||
python-versions = "^3.11"
|
|
||||||
content-hash = "3bccdc3abedf2f8613661596b0d52b5dd0b379a9467934361362eb6987c0efe7"
|
|
||||||
+6
-29
@@ -2,7 +2,7 @@
|
|||||||
name = "md2gost"
|
name = "md2gost"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "Markdown to DOCX (MIREA TZ / GOST 7.32)"
|
description = "Markdown to DOCX (MIREA TZ / GOST 7.32)"
|
||||||
readme = "README.md"
|
readme = "ReadMe.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"python-docx>=0.8.11,<1",
|
"python-docx>=0.8.11,<1",
|
||||||
@@ -16,7 +16,11 @@ dependencies = [
|
|||||||
"lxml>=4.9,<6",
|
"lxml>=4.9,<6",
|
||||||
"mermaidx>=0.9.0,<1",
|
"mermaidx>=0.9.0,<1",
|
||||||
"playwright>=1.40.0,<2",
|
"playwright>=1.40.0,<2",
|
||||||
|
"pywin32; sys_platform == 'win32'",
|
||||||
"data-flow-diagram>=1.16.0,<2",
|
"data-flow-diagram>=1.16.0,<2",
|
||||||
|
# Font lookup: only Linux goes through fc-list, every other platform
|
||||||
|
# imports matplotlib.font_manager (renderable/find_font.py).
|
||||||
|
"matplotlib>=3.7; sys_platform != 'linux'",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
@@ -41,7 +45,7 @@ requires = ["hatchling"]
|
|||||||
build-backend = "hatchling.build"
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
[tool.hatch.build.targets.wheel]
|
[tool.hatch.build.targets.wheel]
|
||||||
packages = ["md2gost", "md2latex", "md2fodt", "word2md"]
|
packages = ["md2gost", "mdexport", "md2latex", "md2fodt", "word2md"]
|
||||||
|
|
||||||
[tool.hatch.build.targets.wheel.force-include]
|
[tool.hatch.build.targets.wheel.force-include]
|
||||||
"md2gost/Template.docx" = "md2gost/Template.docx"
|
"md2gost/Template.docx" = "md2gost/Template.docx"
|
||||||
@@ -64,30 +68,3 @@ packages = ["md2gost", "md2latex", "md2fodt", "word2md"]
|
|||||||
"docs/styles.md" = "md2gost/docs/styles.md"
|
"docs/styles.md" = "md2gost/docs/styles.md"
|
||||||
"docs/prompts.md" = "md2gost/docs/prompts.md"
|
"docs/prompts.md" = "md2gost/docs/prompts.md"
|
||||||
"docs/word2md.md" = "md2gost/docs/word2md.md"
|
"docs/word2md.md" = "md2gost/docs/word2md.md"
|
||||||
|
|
||||||
# Legacy Poetry metadata (optional; prefer uv + [project] above)
|
|
||||||
[tool.poetry]
|
|
||||||
name = "md2gost"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = ""
|
|
||||||
authors = ["witelokk <death.thunder@yandex.ru>"]
|
|
||||||
readme = "README.md"
|
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
|
||||||
python = "^3.11"
|
|
||||||
python-docx = "^0.8.11"
|
|
||||||
marko = "^2.0.0"
|
|
||||||
docxcompose = "^1.4.0"
|
|
||||||
freetype-py = "^2.4.0"
|
|
||||||
pillow = "^10.0.0"
|
|
||||||
matplotlib = { version = "3.7.2", platform = "!= linux" }
|
|
||||||
requests = "^2.31.0"
|
|
||||||
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"
|
|
||||||
md2gost-gui = "md2gost.gui:main"
|
|
||||||
|
|||||||
+19
-5
@@ -1,33 +1,47 @@
|
|||||||
babel==2.18.0
|
babel==2.18.0
|
||||||
|
cairocffi==1.7.1
|
||||||
|
cairosvg==2.9.1
|
||||||
certifi==2026.7.22
|
certifi==2026.7.22
|
||||||
|
cffi==2.1.1
|
||||||
charset-normalizer==3.5.1
|
charset-normalizer==3.5.1
|
||||||
colorama==0.4.6
|
colorama==0.4.6
|
||||||
|
contourpy==1.3.3
|
||||||
|
cssselect2==0.10.1
|
||||||
|
cycler==0.12.1
|
||||||
|
data-flow-diagram==1.17.0
|
||||||
|
defusedxml==0.7.1
|
||||||
docxcompose==1.4.1
|
docxcompose==1.4.1
|
||||||
exceptiongroup==1.3.1
|
fonttools==4.64.0
|
||||||
freetype-py==2.5.1
|
freetype-py==2.5.1
|
||||||
greenlet==3.5.5
|
greenlet==3.5.5
|
||||||
idna==3.19
|
idna==3.19
|
||||||
iniconfig==2.3.0
|
iniconfig==2.3.0
|
||||||
|
kiwisolver==1.5.1
|
||||||
latex2mathml==3.81.0
|
latex2mathml==3.81.0
|
||||||
lxml==5.4.0
|
lxml==5.4.0
|
||||||
marko==2.2.4
|
marko==2.2.4
|
||||||
|
matplotlib==3.11.1
|
||||||
|
-e file:///C:/Users/hlebushek/Desktop/Work/GhostEditor
|
||||||
mermaidx==0.9.5
|
mermaidx==0.9.5
|
||||||
|
numpy==2.5.3
|
||||||
packaging==26.3
|
packaging==26.3
|
||||||
pillow==10.4.0
|
pillow==10.4.0
|
||||||
pip==25.0.1
|
|
||||||
playwright==1.62.0
|
playwright==1.62.0
|
||||||
pluggy==1.6.0
|
pluggy==1.6.0
|
||||||
|
pycparser==3.0
|
||||||
pyee==13.0.1
|
pyee==13.0.1
|
||||||
pygments==2.21.0
|
pygments==2.21.0
|
||||||
|
pyparsing==3.3.2
|
||||||
pytest==9.1.1
|
pytest==9.1.1
|
||||||
|
python-dateutil==2.9.0.post0
|
||||||
python-docx==0.8.11
|
python-docx==0.8.11
|
||||||
|
pywin32==312
|
||||||
quickjs-ng==0.16.2.1
|
quickjs-ng==0.16.2.1
|
||||||
requests==2.34.2
|
requests==2.34.2
|
||||||
resvg-py==0.5.0
|
resvg-py==0.5.0
|
||||||
setuptools==78.0.1
|
|
||||||
six==1.17.0
|
six==1.17.0
|
||||||
termaid==0.8.0
|
termaid==0.8.0
|
||||||
tomli==2.4.1
|
tinycss2==1.5.1
|
||||||
typing-extensions==4.16.0
|
typing-extensions==4.16.0
|
||||||
urllib3==2.7.0
|
urllib3==2.7.0
|
||||||
wheel==0.45.1
|
webencodings==0.6.1
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 69 KiB |
@@ -1,78 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="2400" height="1700" viewBox="0 0 2400 1700">
|
|
||||||
<rect width="100%" height="100%" fill="#fff"/>
|
|
||||||
<rect x="16.0" y="16.0" width="2368.0" height="1668.0" fill="none" stroke="#000000" stroke-width="2.00"/>
|
|
||||||
<rect x="22.4" y="22.4" width="2355.2" height="1655.2" fill="none" stroke="#000000" stroke-width="2.00"/>
|
|
||||||
<line x1="22.4" y1="214.4" x2="2377.6" y2="214.4" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<line x1="22.4" y1="1549.6" x2="2377.6" y2="1549.6" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<line x1="363.9" y1="22.4" x2="363.9" y2="214.4" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<line x1="1200.0" y1="22.4" x2="1200.0" y2="214.4" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<line x1="1612.2" y1="22.4" x2="1612.2" y2="214.4" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<line x1="1953.7" y1="22.4" x2="1953.7" y2="214.4" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<line x1="1200.0" y1="66.4" x2="1612.2" y2="66.4" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<line x1="1426.7" y1="22.4" x2="1426.7" y2="66.4" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<text x="34.4" y="32.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">USED AT:</text>
|
|
||||||
<text x="375.9" y="30.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">AUTHOR:</text>
|
|
||||||
<text x="487.9" y="30.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="22.0" font-weight="400" fill="#000000">Кинзябулатов Рамиль</text>
|
|
||||||
<text x="375.9" y="66.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">PROJECT:</text>
|
|
||||||
<text x="503.9" y="66.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">Разработка универсальной модели торгового предприятия</text>
|
|
||||||
<text x="375.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">NOTES:</text>
|
|
||||||
<text x="479.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">1</text>
|
|
||||||
<text x="507.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">2</text>
|
|
||||||
<text x="535.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">3</text>
|
|
||||||
<text x="563.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">4</text>
|
|
||||||
<text x="591.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">5</text>
|
|
||||||
<text x="619.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">6</text>
|
|
||||||
<text x="647.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">7</text>
|
|
||||||
<text x="675.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">8</text>
|
|
||||||
<text x="703.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">9</text>
|
|
||||||
<text x="731.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">10</text>
|
|
||||||
<text x="1210.0" y="30.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">DATE:</text>
|
|
||||||
<text x="1284.0" y="30.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="22.0" font-weight="400" fill="#000000">08.02.2022</text>
|
|
||||||
<text x="1434.7" y="30.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">REV:</text>
|
|
||||||
<text x="1498.7" y="30.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="22.0" font-weight="400" fill="#000000">31.03.2022</text>
|
|
||||||
<rect x="1212.0" y="80.4" width="16.0" height="16.0" fill="none" stroke="#000000" stroke-width="2.00"/>
|
|
||||||
<text x="1236.0" y="78.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">WORKING</text>
|
|
||||||
<rect x="1212.0" y="112.4" width="16.0" height="16.0" fill="none" stroke="#000000" stroke-width="2.00"/>
|
|
||||||
<text x="1236.0" y="110.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">DRAFT</text>
|
|
||||||
<rect x="1212.0" y="144.4" width="16.0" height="16.0" fill="none" stroke="#000000" stroke-width="2.00"/>
|
|
||||||
<text x="1236.0" y="142.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">RECOMMENDED</text>
|
|
||||||
<rect x="1212.0" y="176.4" width="16.0" height="16.0" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
|
||||||
<text x="1236.0" y="174.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="700" fill="#000000">PUBLICATION</text>
|
|
||||||
<text x="1624.2" y="30.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">READER</text>
|
|
||||||
<line x1="1612.2" y1="66.4" x2="1953.7" y2="66.4" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<text x="1624.2" y="74.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">DATE</text>
|
|
||||||
<text x="1965.7" y="30.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">CONTEXT:</text>
|
|
||||||
<text x="2165.6" y="134.4" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="700" fill="#000000">TOP</text>
|
|
||||||
<line x1="446.3" y1="1549.6" x2="446.3" y2="1677.6" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<line x1="1906.6" y1="1549.6" x2="1906.6" y2="1677.6" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<text x="34.4" y="1557.6" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">NODE:</text>
|
|
||||||
<text x="234.4" y="1625.6" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="40.0" font-weight="700" fill="#000000">A-0</text>
|
|
||||||
<text x="458.3" y="1557.6" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">TITLE:</text>
|
|
||||||
<text x="1176.4" y="1625.6" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="32.0" font-weight="700" fill="#000000">Распорядиться товаром</text>
|
|
||||||
<text x="1918.6" y="1557.6" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">NUMBER:</text>
|
|
||||||
<text x="2142.1" y="1621.6" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="40.0" font-weight="700" fill="#000000">1</text>
|
|
||||||
<rect x="2341.6" y="1641.6" width="36.0" height="36.0" fill="none" stroke="#000000" stroke-width="2.00"/>
|
|
||||||
<text x="2359.6" y="1659.6" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="700" fill="#000000">1</text>
|
|
||||||
<line x1="38.4" y1="870.0" x2="888.0" y2="870.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<polygon points="888.0,870.0 872.0,863.3 872.0,876.7" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
|
||||||
<line x1="1208.0" y1="230.4" x2="1208.0" y2="732.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<polygon points="1208.0,732.0 1201.3,716.0 1214.7,716.0" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
|
||||||
<line x1="1528.0" y1="870.0" x2="2361.6" y2="870.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<polygon points="2361.6,870.0 2345.6,863.3 2345.6,876.7" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
|
||||||
<line x1="1208.0" y1="1533.6" x2="1208.0" y2="1008.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<polygon points="1208.0,1008.0 1201.3,1024.0 1214.7,1024.0" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
|
||||||
<rect x="406.6" y="799.9" width="79.2" height="38.1" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
|
||||||
<text x="446.2" y="814.2" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">Спрос</text>
|
|
||||||
<rect x="1007.8" y="438.7" width="168.2" height="65.0" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
|
||||||
<text x="1091.9" y="452.9" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">Нормативная</text>
|
|
||||||
<text x="1091.9" y="479.8" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">документация</text>
|
|
||||||
<rect x="1889.0" y="799.9" width="78.2" height="38.1" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
|
||||||
<text x="1928.1" y="814.2" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">Товар</text>
|
|
||||||
<rect x="1056.8" y="1262.3" width="119.2" height="38.1" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
|
||||||
<text x="1116.4" y="1276.5" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">Персонал</text>
|
|
||||||
<rect x="896.0" y="740.0" width="640.0" height="276.0" fill="#c0c0c0" stroke="none" stroke-width="0.00"/>
|
|
||||||
<rect x="888.0" y="732.0" width="640.0" height="276.0" fill="#ffffff" stroke="#000000" stroke-width="2.00"/>
|
|
||||||
<polygon points="888.0,732.0 904.0,732.0 888.0,748.0" fill="#000000" stroke="#000000" stroke-width="1.00"/>
|
|
||||||
<text x="1208.0" y="864.1" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="26.0" font-weight="400" fill="#000000">Распорядиться товаром</text>
|
|
||||||
<text x="1516.0" y="998.0" dx="0" dy="0" text-anchor="end" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="20.0" font-weight="400" fill="#000000">A0</text>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 114 KiB |
@@ -1,166 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="2800" height="1840" viewBox="0 0 2800 1840">
|
|
||||||
<rect width="100%" height="100%" fill="#fff"/>
|
|
||||||
<rect x="16.0" y="16.0" width="2768.0" height="1808.0" fill="none" stroke="#000000" stroke-width="2.00"/>
|
|
||||||
<rect x="22.4" y="22.4" width="2755.2" height="1795.2" fill="none" stroke="#000000" stroke-width="2.00"/>
|
|
||||||
<line x1="22.4" y1="214.4" x2="2777.6" y2="214.4" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<line x1="22.4" y1="1689.6" x2="2777.6" y2="1689.6" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<line x1="421.9" y1="22.4" x2="421.9" y2="214.4" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<line x1="1400.0" y1="22.4" x2="1400.0" y2="214.4" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<line x1="1882.2" y1="22.4" x2="1882.2" y2="214.4" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<line x1="2281.7" y1="22.4" x2="2281.7" y2="214.4" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<line x1="1400.0" y1="66.4" x2="1882.2" y2="66.4" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<line x1="1665.2" y1="22.4" x2="1665.2" y2="66.4" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<text x="34.4" y="32.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">USED AT:</text>
|
|
||||||
<text x="433.9" y="30.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">AUTHOR:</text>
|
|
||||||
<text x="545.9" y="30.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="22.0" font-weight="400" fill="#000000">Кинзябулатов Рамиль</text>
|
|
||||||
<text x="433.9" y="66.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">PROJECT:</text>
|
|
||||||
<text x="561.9" y="66.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">Разработка универсальной модели торгового предприятия</text>
|
|
||||||
<text x="433.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">NOTES:</text>
|
|
||||||
<text x="537.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">1</text>
|
|
||||||
<text x="565.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">2</text>
|
|
||||||
<text x="593.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">3</text>
|
|
||||||
<text x="621.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">4</text>
|
|
||||||
<text x="649.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">5</text>
|
|
||||||
<text x="677.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">6</text>
|
|
||||||
<text x="705.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">7</text>
|
|
||||||
<text x="733.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">8</text>
|
|
||||||
<text x="761.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">9</text>
|
|
||||||
<text x="789.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">10</text>
|
|
||||||
<text x="1410.0" y="30.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">DATE:</text>
|
|
||||||
<text x="1484.0" y="30.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="22.0" font-weight="400" fill="#000000">08.02.2022</text>
|
|
||||||
<text x="1673.2" y="30.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">REV:</text>
|
|
||||||
<text x="1737.2" y="30.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="22.0" font-weight="400" fill="#000000">31.03.2022</text>
|
|
||||||
<rect x="1412.0" y="80.4" width="16.0" height="16.0" fill="none" stroke="#000000" stroke-width="2.00"/>
|
|
||||||
<text x="1436.0" y="78.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">WORKING</text>
|
|
||||||
<rect x="1412.0" y="112.4" width="16.0" height="16.0" fill="none" stroke="#000000" stroke-width="2.00"/>
|
|
||||||
<text x="1436.0" y="110.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">DRAFT</text>
|
|
||||||
<rect x="1412.0" y="144.4" width="16.0" height="16.0" fill="none" stroke="#000000" stroke-width="2.00"/>
|
|
||||||
<text x="1436.0" y="142.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">RECOMMENDED</text>
|
|
||||||
<rect x="1412.0" y="176.4" width="16.0" height="16.0" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
|
||||||
<text x="1436.0" y="174.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="700" fill="#000000">PUBLICATION</text>
|
|
||||||
<text x="1894.2" y="30.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">READER</text>
|
|
||||||
<line x1="1882.2" y1="66.4" x2="2281.7" y2="66.4" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<text x="1894.2" y="74.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">DATE</text>
|
|
||||||
<text x="2293.7" y="30.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">CONTEXT:</text>
|
|
||||||
<rect x="2475.6" y="90.4" width="116.0" height="56.0" fill="#c4c4c4" stroke="none" stroke-width="0.00"/>
|
|
||||||
<rect x="2471.6" y="86.4" width="116.0" height="56.0" fill="#ffffff" stroke="#000000" stroke-width="2.00"/>
|
|
||||||
<text x="2579.6" y="136.4" dx="0" dy="0" text-anchor="end" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="700" fill="#000000">A-0</text>
|
|
||||||
<line x1="518.3" y1="1689.6" x2="518.3" y2="1817.6" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<line x1="2226.6" y1="1689.6" x2="2226.6" y2="1817.6" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<text x="34.4" y="1697.6" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">NODE:</text>
|
|
||||||
<text x="270.4" y="1765.6" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="40.0" font-weight="700" fill="#000000">A0</text>
|
|
||||||
<text x="530.3" y="1697.6" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">TITLE:</text>
|
|
||||||
<text x="1372.4" y="1765.6" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="32.0" font-weight="700" fill="#000000">Распорядиться товаром</text>
|
|
||||||
<text x="2238.6" y="1697.6" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">NUMBER:</text>
|
|
||||||
<text x="2502.1" y="1761.6" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="40.0" font-weight="700" fill="#000000">2</text>
|
|
||||||
<rect x="2741.6" y="1781.6" width="36.0" height="36.0" fill="none" stroke="#000000" stroke-width="2.00"/>
|
|
||||||
<text x="2759.6" y="1799.6" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="700" fill="#000000">2</text>
|
|
||||||
<line x1="38.4" y1="652.0" x2="696.0" y2="652.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<polygon points="696.0,652.0 680.0,645.3 680.0,658.7" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
|
||||||
<line x1="38.4" y1="891.8" x2="1208.0" y2="891.8" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<polygon points="1208.0,891.8 1192.0,885.1 1192.0,898.6" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
|
||||||
<line x1="1296.0" y1="230.4" x2="1296.0" y2="278.4" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<line x1="1296.0" y1="278.4" x2="784.0" y2="278.4" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<line x1="784.0" y1="278.4" x2="784.0" y2="566.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<line x1="1296.0" y1="230.4" x2="1296.0" y2="854.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<line x1="1296.0" y1="278.4" x2="1808.0" y2="278.4" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<line x1="1808.0" y1="278.4" x2="1808.0" y2="1142.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<polygon points="784.0,566.0 777.3,550.0 790.7,550.0" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
|
||||||
<polygon points="1296.0,854.0 1289.3,838.0 1302.7,838.0" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
|
||||||
<polygon points="1808.0,1142.0 1801.3,1126.0 1814.7,1126.0" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
|
||||||
<line x1="1008.0" y1="230.4" x2="1008.0" y2="566.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<polygon points="1008.0,566.0 1001.3,550.0 1014.7,550.0" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
|
||||||
<line x1="2032.0" y1="230.4" x2="2032.0" y2="1142.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<polygon points="2032.0,1142.0 2025.3,1126.0 2038.7,1126.0" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
|
||||||
<line x1="1096.0" y1="603.8" x2="1152.0" y2="603.8" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<line x1="1152.0" y1="603.8" x2="1152.0" y2="988.2" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<line x1="1152.0" y1="988.2" x2="1208.0" y2="988.2" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<polygon points="1208.0,988.2 1192.0,981.4 1192.0,994.9" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
|
||||||
<line x1="1096.0" y1="700.2" x2="1216.0" y2="700.2" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<line x1="1216.0" y1="700.2" x2="1216.0" y2="798.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<line x1="1216.0" y1="798.0" x2="1520.0" y2="798.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<line x1="1520.0" y1="798.0" x2="1520.0" y2="854.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<polygon points="1520.0,854.0 1513.3,838.0 1526.7,838.0" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
|
||||||
<line x1="1608.0" y1="940.0" x2="1664.0" y2="940.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<line x1="1664.0" y1="940.0" x2="1664.0" y2="1228.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<line x1="1664.0" y1="1228.0" x2="1720.0" y2="1228.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<polygon points="1720.0,1228.0 1704.0,1221.3 1704.0,1234.7" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
|
||||||
<line x1="2120.0" y1="1179.8" x2="2761.6" y2="1179.8" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<polygon points="2761.6,1179.8 2745.6,1173.1 2745.6,1186.6" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
|
||||||
<line x1="2120.0" y1="1276.2" x2="2761.6" y2="1276.2" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<polygon points="2761.6,1276.2 2745.6,1269.4 2745.6,1282.9" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
|
||||||
<line x1="1408.0" y1="1673.6" x2="1408.0" y2="1362.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<line x1="1408.0" y1="1362.0" x2="896.0" y2="1362.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<line x1="896.0" y1="1362.0" x2="896.0" y2="738.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<line x1="1408.0" y1="1362.0" x2="1296.0" y2="1362.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<line x1="1296.0" y1="1362.0" x2="1296.0" y2="1026.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<line x1="1408.0" y1="1362.0" x2="1920.0" y2="1362.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<line x1="1920.0" y1="1362.0" x2="1920.0" y2="1314.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<polygon points="896.0,738.0 889.3,754.0 902.7,754.0" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
|
||||||
<polygon points="1296.0,1026.0 1289.3,1042.0 1302.7,1042.0" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
|
||||||
<polygon points="1920.0,1314.0 1913.3,1330.0 1926.7,1330.0" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
|
||||||
<line x1="1520.0" y1="1673.6" x2="1520.0" y2="1026.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
|
||||||
<polygon points="1520.0,1026.0 1513.3,1042.0 1526.7,1042.0" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
|
||||||
<rect x="42.4" y="627.0" width="23.4" height="21.4" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
|
||||||
<text x="54.1" y="637.7" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="20.0" font-weight="700" fill="#000000">I1</text>
|
|
||||||
<rect x="42.4" y="866.8" width="23.4" height="21.4" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
|
||||||
<text x="54.1" y="877.5" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="20.0" font-weight="700" fill="#000000">I2</text>
|
|
||||||
<rect x="1306.0" y="234.4" width="31.4" height="21.4" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
|
||||||
<text x="1321.7" y="245.1" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="20.0" font-weight="700" fill="#000000">C1</text>
|
|
||||||
<rect x="1018.0" y="234.4" width="31.4" height="21.4" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
|
||||||
<text x="1033.7" y="245.1" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="20.0" font-weight="700" fill="#000000">C2</text>
|
|
||||||
<rect x="2042.0" y="234.4" width="31.4" height="21.4" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
|
||||||
<text x="2057.7" y="245.1" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="20.0" font-weight="700" fill="#000000">C3</text>
|
|
||||||
<rect x="2724.2" y="1154.8" width="33.4" height="21.4" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
|
||||||
<text x="2740.9" y="1165.5" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="20.0" font-weight="700" fill="#000000">O1</text>
|
|
||||||
<rect x="2724.2" y="1251.2" width="33.4" height="21.4" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
|
||||||
<text x="2740.9" y="1261.9" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="20.0" font-weight="700" fill="#000000">O2</text>
|
|
||||||
<rect x="1418.0" y="1648.2" width="34.4" height="21.4" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
|
||||||
<text x="1435.2" y="1658.9" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="20.0" font-weight="700" fill="#000000">M1</text>
|
|
||||||
<rect x="1530.0" y="1648.2" width="34.4" height="21.4" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
|
||||||
<text x="1547.2" y="1658.9" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="20.0" font-weight="700" fill="#000000">M2</text>
|
|
||||||
<rect x="314.4" y="581.9" width="79.2" height="38.1" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
|
||||||
<text x="354.0" y="596.2" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">Спрос</text>
|
|
||||||
<rect x="518.2" y="794.9" width="163.2" height="65.0" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
|
||||||
<text x="599.8" y="809.1" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">Предложения</text>
|
|
||||||
<text x="599.8" y="836.0" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">поставщиков</text>
|
|
||||||
<rect x="1211.9" y="295.0" width="168.2" height="65.0" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
|
||||||
<text x="1296.0" y="309.3" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">Нормативная</text>
|
|
||||||
<text x="1296.0" y="336.2" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">документация</text>
|
|
||||||
<rect x="1040.0" y="392.6" width="195.2" height="65.0" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
|
||||||
<text x="1137.6" y="406.8" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">Ассортиментная</text>
|
|
||||||
<text x="1137.6" y="433.7" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">политика</text>
|
|
||||||
<rect x="1889.8" y="635.5" width="110.2" height="65.0" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
|
||||||
<text x="1944.9" y="649.7" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">Условия</text>
|
|
||||||
<text x="1944.9" y="676.6" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">поставки</text>
|
|
||||||
<rect x="1122.9" y="533.8" width="155.2" height="38.1" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
|
||||||
<text x="1200.5" y="548.0" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">план закупок</text>
|
|
||||||
<rect x="1165.6" y="603.2" width="110.2" height="65.0" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
|
||||||
<text x="1220.7" y="617.4" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">график</text>
|
|
||||||
<text x="1220.7" y="644.3" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">поставок</text>
|
|
||||||
<rect x="1634.9" y="869.9" width="192.2" height="38.1" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
|
||||||
<text x="1731.0" y="884.2" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">товар к отгрузке</text>
|
|
||||||
<rect x="2388.9" y="1109.8" width="78.2" height="38.1" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
|
||||||
<text x="2428.0" y="1124.0" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">Товар</text>
|
|
||||||
<rect x="2352.9" y="1206.1" width="150.2" height="38.1" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
|
||||||
<text x="2428.0" y="1220.3" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">Акт отгрузки</text>
|
|
||||||
<rect x="1256.8" y="1505.0" width="119.2" height="38.1" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
|
||||||
<text x="1316.4" y="1519.2" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">Персонал</text>
|
|
||||||
<rect x="1407.8" y="1279.0" width="80.2" height="38.1" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
|
||||||
<text x="1447.9" y="1293.2" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">Склад</text>
|
|
||||||
<rect x="704.0" y="574.0" width="400.0" height="172.0" fill="#c0c0c0" stroke="none" stroke-width="0.00"/>
|
|
||||||
<rect x="696.0" y="566.0" width="400.0" height="172.0" fill="#ffffff" stroke="#000000" stroke-width="2.00"/>
|
|
||||||
<polygon points="696.0,566.0 712.0,566.0 696.0,582.0" fill="#000000" stroke="#000000" stroke-width="1.00"/>
|
|
||||||
<text x="896.0" y="646.1" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="26.0" font-weight="400" fill="#000000">Принять и оценить спрос</text>
|
|
||||||
<text x="1084.0" y="728.0" dx="0" dy="0" text-anchor="end" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="20.0" font-weight="400" fill="#000000">A1</text>
|
|
||||||
<rect x="1216.0" y="862.0" width="400.0" height="172.0" fill="#c0c0c0" stroke="none" stroke-width="0.00"/>
|
|
||||||
<rect x="1208.0" y="854.0" width="400.0" height="172.0" fill="#ffffff" stroke="#000000" stroke-width="2.00"/>
|
|
||||||
<polygon points="1208.0,854.0 1224.0,854.0 1208.0,870.0" fill="#000000" stroke="#000000" stroke-width="1.00"/>
|
|
||||||
<text x="1408.0" y="934.1" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="26.0" font-weight="400" fill="#000000">Закупить и хранить</text>
|
|
||||||
<text x="1596.0" y="1016.0" dx="0" dy="0" text-anchor="end" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="20.0" font-weight="400" fill="#000000">A2</text>
|
|
||||||
<rect x="1728.0" y="1150.0" width="400.0" height="172.0" fill="#c0c0c0" stroke="none" stroke-width="0.00"/>
|
|
||||||
<rect x="1720.0" y="1142.0" width="400.0" height="172.0" fill="#ffffff" stroke="#000000" stroke-width="2.00"/>
|
|
||||||
<polygon points="1720.0,1142.0 1736.0,1142.0 1720.0,1158.0" fill="#000000" stroke="#000000" stroke-width="1.00"/>
|
|
||||||
<text x="1920.0" y="1222.1" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="26.0" font-weight="400" fill="#000000">Отгрузить товар</text>
|
|
||||||
<text x="2108.0" y="1304.0" dx="0" dy="0" text-anchor="end" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="20.0" font-weight="400" fill="#000000">A3</text>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 24 KiB |
@@ -112,6 +112,7 @@ def test_write_dfd_outputs_uses_local_or_kroki(tmp_path):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
with patch("md2gost.dfd._compile_dot", return_value=(fake_dot, False)), \
|
with patch("md2gost.dfd._compile_dot", return_value=(fake_dot, False)), \
|
||||||
|
patch("md2gost.dfd._render_dot_local_combined", return_value=set()), \
|
||||||
patch("md2gost.dfd._render_dot_local", side_effect=fake_local), \
|
patch("md2gost.dfd._render_dot_local", side_effect=fake_local), \
|
||||||
patch("md2gost.dfd._render_dot_kroki", return_value=False):
|
patch("md2gost.dfd._render_dot_kroki", return_value=False):
|
||||||
assert write_dfd_outputs(SAMPLE_LEVEL0, png, svg, scale=1)
|
assert write_dfd_outputs(SAMPLE_LEVEL0, png, svg, scale=1)
|
||||||
@@ -119,6 +120,64 @@ def test_write_dfd_outputs_uses_local_or_kroki(tmp_path):
|
|||||||
assert "Клиент" in svg.read_text(encoding="utf-8")
|
assert "Клиент" in svg.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def test_write_dfd_outputs_one_graphviz_run_for_both_formats(tmp_path):
|
||||||
|
"""PNG + SVG come from a single Graphviz process; no per-format retry."""
|
||||||
|
png = tmp_path / "a.png"
|
||||||
|
svg = tmp_path / "a.svg"
|
||||||
|
single = []
|
||||||
|
|
||||||
|
def fake_combined(dot, targets, *, context):
|
||||||
|
for out_path, fmt in targets:
|
||||||
|
Path(out_path).write_bytes(
|
||||||
|
b"\x89PNG\r\n\x1a\n" + b"\x00" * 32 if fmt == "png" else b"<svg/>"
|
||||||
|
)
|
||||||
|
return {fmt for _, fmt in targets}
|
||||||
|
|
||||||
|
with patch("md2gost.dfd._compile_dot", return_value=("digraph G {}", False)), \
|
||||||
|
patch("md2gost.dfd._render_dot_local_combined", side_effect=fake_combined), \
|
||||||
|
patch("md2gost.dfd._render_dot_local", side_effect=lambda *a, **k: single.append(a) or True), \
|
||||||
|
patch("md2gost.dfd._render_dot_kroki", return_value=False):
|
||||||
|
assert write_dfd_outputs(SAMPLE_LEVEL0, png, svg, scale=1)
|
||||||
|
assert single == []
|
||||||
|
assert png.read_bytes().startswith(b"\x89PNG")
|
||||||
|
assert svg.read_bytes().startswith(b"<svg")
|
||||||
|
|
||||||
|
|
||||||
|
def test_write_dfd_outputs_retries_format_missed_by_combined(tmp_path):
|
||||||
|
"""If the combined run only produced PNG, SVG still goes through the old path."""
|
||||||
|
png = tmp_path / "a.png"
|
||||||
|
svg = tmp_path / "a.svg"
|
||||||
|
retried = []
|
||||||
|
|
||||||
|
def fake_combined(dot, targets, *, context):
|
||||||
|
for out_path, fmt in targets:
|
||||||
|
if fmt == "png":
|
||||||
|
Path(out_path).write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 32)
|
||||||
|
return {"png"}
|
||||||
|
|
||||||
|
def fake_local(dot, out_path, fmt, *, context):
|
||||||
|
retried.append(fmt)
|
||||||
|
Path(out_path).write_bytes(b"<svg/>")
|
||||||
|
return True
|
||||||
|
|
||||||
|
with patch("md2gost.dfd._compile_dot", return_value=("digraph G {}", False)), \
|
||||||
|
patch("md2gost.dfd._render_dot_local_combined", side_effect=fake_combined), \
|
||||||
|
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 retried == ["svg"]
|
||||||
|
assert svg.read_bytes().startswith(b"<svg")
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_dot_local_combined_single_target_is_noop():
|
||||||
|
from md2gost.dfd import _render_dot_local_combined
|
||||||
|
|
||||||
|
with patch("md2gost.dfd.resolve_graphviz_engine") as resolve:
|
||||||
|
assert _render_dot_local_combined("digraph G {}", [(Path("a.png"), "png")],
|
||||||
|
context=False) == set()
|
||||||
|
resolve.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
def test_write_dfd_outputs_falls_back_to_kroki(tmp_path):
|
def test_write_dfd_outputs_falls_back_to_kroki(tmp_path):
|
||||||
png = tmp_path / "a.png"
|
png = tmp_path / "a.png"
|
||||||
|
|
||||||
|
|||||||
@@ -16,11 +16,18 @@ from md2gost.diagram_includes import (
|
|||||||
)
|
)
|
||||||
from md2gost.diagram_renderer import (
|
from md2gost.diagram_renderer import (
|
||||||
DIAGRAM_LANGS,
|
DIAGRAM_LANGS,
|
||||||
|
PLANTUML_CACHE_PREFIX,
|
||||||
|
PLANTUML_VERSION,
|
||||||
|
_cache_paths,
|
||||||
|
_PumlJob,
|
||||||
|
_plantuml_run,
|
||||||
configure_diagrams,
|
configure_diagrams,
|
||||||
diagram_engine_status,
|
diagram_engine_status,
|
||||||
is_diagram_lang,
|
is_diagram_lang,
|
||||||
prepare_source,
|
prepare_source,
|
||||||
|
register_pending_diagram,
|
||||||
render_diagram,
|
render_diagram,
|
||||||
|
reset_diagram_batch,
|
||||||
resolve_plantuml_jar,
|
resolve_plantuml_jar,
|
||||||
)
|
)
|
||||||
from md2gost.diagram_schemes import (
|
from md2gost.diagram_schemes import (
|
||||||
@@ -558,6 +565,222 @@ def test_attach_svg_blip():
|
|||||||
assert len(image_rels) >= 2
|
assert len(image_rels) >= 2
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeProc:
|
||||||
|
returncode = 0
|
||||||
|
stdout = ""
|
||||||
|
stderr = ""
|
||||||
|
|
||||||
|
|
||||||
|
def _fake_java(*, out_stem: str = "src", fail: bool = False):
|
||||||
|
"""Stand-in for `java -jar plantuml.jar`: writes what the real jar would."""
|
||||||
|
calls: list[list[str]] = []
|
||||||
|
|
||||||
|
def run(cmd, **kwargs):
|
||||||
|
calls.append(list(cmd))
|
||||||
|
if fail:
|
||||||
|
proc = _FakeProc()
|
||||||
|
proc.returncode = 1
|
||||||
|
proc.stderr = "boom"
|
||||||
|
return proc
|
||||||
|
fmt = next(a[2:] for a in cmd if a.startswith("-t"))
|
||||||
|
payload = (
|
||||||
|
b"\x89PNG\r\n\x1a\n" + b"\x00" * 32 if fmt == "png"
|
||||||
|
else b"<svg xmlns='http://www.w3.org/2000/svg'/>"
|
||||||
|
)
|
||||||
|
for arg in cmd:
|
||||||
|
if not str(arg).endswith(".puml"):
|
||||||
|
continue
|
||||||
|
out_dir = Path(arg).parent / "out"
|
||||||
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
(out_dir / f"{out_stem}.{fmt}").write_bytes(payload)
|
||||||
|
return _FakeProc()
|
||||||
|
|
||||||
|
return run, calls
|
||||||
|
|
||||||
|
|
||||||
|
def _dummy_jar(tmp_path: Path) -> Path:
|
||||||
|
jar = tmp_path / "plantuml.jar"
|
||||||
|
jar.write_bytes(b"PK" + b"\x00" * 2000)
|
||||||
|
return jar
|
||||||
|
|
||||||
|
|
||||||
|
def test_plantuml_run_batches_sources_into_one_jvm(tmp_path):
|
||||||
|
jar = _dummy_jar(tmp_path)
|
||||||
|
jobs = [_PumlJob(f"@startuml\nA{i} -> B{i}\n@enduml", tmp_path / f"{i}.png")
|
||||||
|
for i in range(3)]
|
||||||
|
run, calls = _fake_java()
|
||||||
|
with patch("md2gost.diagram_renderer.subprocess.run", side_effect=run), \
|
||||||
|
patch("md2gost.diagram_renderer._java_available", return_value=True):
|
||||||
|
done = _plantuml_run(jobs, str(jar), "png")
|
||||||
|
assert done == [True, True, True]
|
||||||
|
assert len(calls) == 1
|
||||||
|
assert sum(1 for a in calls[0] if str(a).endswith(".puml")) == 3
|
||||||
|
for job in jobs:
|
||||||
|
assert job.out_path.read_bytes().startswith(b"\x89PNG")
|
||||||
|
|
||||||
|
|
||||||
|
def test_plantuml_run_keeps_named_diagrams_apart(tmp_path):
|
||||||
|
"""`@startuml Name` renames the output file — each source needs its own -o dir."""
|
||||||
|
jar = _dummy_jar(tmp_path)
|
||||||
|
jobs = [_PumlJob("@startuml Name\nA -> B\n@enduml", tmp_path / f"{i}.png")
|
||||||
|
for i in range(2)]
|
||||||
|
run, calls = _fake_java(out_stem="Name")
|
||||||
|
with patch("md2gost.diagram_renderer.subprocess.run", side_effect=run), \
|
||||||
|
patch("md2gost.diagram_renderer._java_available", return_value=True):
|
||||||
|
done = _plantuml_run(jobs, str(jar), "png")
|
||||||
|
assert done == [True, True]
|
||||||
|
assert calls[0].count("-o") == 1
|
||||||
|
src_dirs = {Path(a).parent for a in calls[0] if str(a).endswith(".puml")}
|
||||||
|
assert len(src_dirs) == 2
|
||||||
|
assert all(job.out_path.is_file() for job in jobs)
|
||||||
|
|
||||||
|
|
||||||
|
def test_plantuml_run_failure_reports_every_job(tmp_path):
|
||||||
|
jar = _dummy_jar(tmp_path)
|
||||||
|
jobs = [_PumlJob("@startuml\nA -> B\n@enduml", tmp_path / f"{i}.png")
|
||||||
|
for i in range(2)]
|
||||||
|
run, _calls = _fake_java(fail=True)
|
||||||
|
with patch("md2gost.diagram_renderer.subprocess.run", side_effect=run), \
|
||||||
|
patch("md2gost.diagram_renderer._java_available", return_value=True):
|
||||||
|
done = _plantuml_run(jobs, str(jar), "png")
|
||||||
|
assert done == [False, False]
|
||||||
|
assert not any(job.out_path.exists() for job in jobs)
|
||||||
|
|
||||||
|
|
||||||
|
def test_plantuml_run_without_java_never_spawns(tmp_path):
|
||||||
|
jar = _dummy_jar(tmp_path)
|
||||||
|
run, calls = _fake_java()
|
||||||
|
with patch("md2gost.diagram_renderer.subprocess.run", side_effect=run), \
|
||||||
|
patch("md2gost.diagram_renderer._java_available", return_value=False):
|
||||||
|
done = _plantuml_run([_PumlJob("@startuml\nA -> B\n@enduml", tmp_path / "a.png")],
|
||||||
|
str(jar), "png")
|
||||||
|
assert done == [False]
|
||||||
|
assert calls == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_batch_prepass_draws_whole_document_in_one_jvm(tmp_path):
|
||||||
|
jar = _dummy_jar(tmp_path)
|
||||||
|
configure_diagrams(
|
||||||
|
fallback="off", cache_dir=str(tmp_path), plantuml_jar=str(jar), diagram_scale=1,
|
||||||
|
)
|
||||||
|
register_pending_diagram("uml", "A -> B")
|
||||||
|
register_pending_diagram("uml", "C -> D")
|
||||||
|
run, calls = _fake_java()
|
||||||
|
with patch("md2gost.diagram_renderer.subprocess.run", side_effect=run), \
|
||||||
|
patch("md2gost.diagram_renderer._java_available", return_value=True):
|
||||||
|
first = render_diagram("uml", "A -> B")
|
||||||
|
second = render_diagram("uml", "C -> D")
|
||||||
|
assert len(calls) == 1
|
||||||
|
assert first.png_path != second.png_path
|
||||||
|
for result in (first, second):
|
||||||
|
assert Path(result.png_path).read_bytes().startswith(b"\x89PNG")
|
||||||
|
|
||||||
|
|
||||||
|
def test_batch_prepass_svg_mode_uses_two_jvms_not_four(tmp_path):
|
||||||
|
jar = _dummy_jar(tmp_path)
|
||||||
|
configure_diagrams(
|
||||||
|
fallback="off", cache_dir=str(tmp_path), plantuml_jar=str(jar),
|
||||||
|
diagram_scale=1, diagram_format="svg",
|
||||||
|
)
|
||||||
|
register_pending_diagram("uml", "A -> B")
|
||||||
|
register_pending_diagram("uml", "C -> D")
|
||||||
|
run, calls = _fake_java()
|
||||||
|
with patch("md2gost.diagram_renderer.subprocess.run", side_effect=run), \
|
||||||
|
patch("md2gost.diagram_renderer._java_available", return_value=True):
|
||||||
|
first = render_diagram("uml", "A -> B")
|
||||||
|
second = render_diagram("uml", "C -> D")
|
||||||
|
assert sorted(next(a for a in c if a.startswith("-t")) for c in calls) == \
|
||||||
|
["-tpng", "-tsvg"]
|
||||||
|
assert first.svg_path and second.svg_path
|
||||||
|
assert Path(first.svg_path).read_bytes().startswith(b"<svg")
|
||||||
|
|
||||||
|
|
||||||
|
def test_batch_prepass_skips_cached_diagrams(tmp_path):
|
||||||
|
jar = _dummy_jar(tmp_path)
|
||||||
|
configure_diagrams(
|
||||||
|
fallback="off", cache_dir=str(tmp_path), plantuml_jar=str(jar), diagram_scale=1,
|
||||||
|
)
|
||||||
|
cached_src, _ = prepare_source("uml", "A -> B")
|
||||||
|
cached_png, _ = _cache_paths(
|
||||||
|
cached_src, str(tmp_path), cache_prefix=PLANTUML_CACHE_PREFIX,
|
||||||
|
)
|
||||||
|
cached_png.write_bytes(b"\x89PNG\r\n\x1a\n" + b"cached")
|
||||||
|
register_pending_diagram("uml", "A -> B")
|
||||||
|
register_pending_diagram("uml", "C -> D")
|
||||||
|
run, calls = _fake_java()
|
||||||
|
with patch("md2gost.diagram_renderer.subprocess.run", side_effect=run), \
|
||||||
|
patch("md2gost.diagram_renderer._java_available", return_value=True):
|
||||||
|
hit = render_diagram("uml", "A -> B")
|
||||||
|
render_diagram("uml", "C -> D")
|
||||||
|
assert len(calls) == 1
|
||||||
|
assert sum(1 for a in calls[0] if str(a).endswith(".puml")) == 1
|
||||||
|
assert Path(hit.png_path).read_bytes().endswith(b"cached")
|
||||||
|
|
||||||
|
|
||||||
|
def test_batch_prepass_not_triggered_for_explicit_overrides(tmp_path):
|
||||||
|
configure_diagrams(fallback="remote", cache_dir=str(tmp_path))
|
||||||
|
register_pending_diagram("uml", "A -> B")
|
||||||
|
|
||||||
|
def fake_kroki(source, diagram_type, base_url, out_path, fmt="png"):
|
||||||
|
out_path.write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 20)
|
||||||
|
return True
|
||||||
|
|
||||||
|
with patch("md2gost.diagram_renderer._flush_plantuml_batch") as flush, \
|
||||||
|
patch("md2gost.diagram_renderer._render_plantuml_jar", return_value=False), \
|
||||||
|
patch("md2gost.diagram_renderer._render_kroki", side_effect=fake_kroki):
|
||||||
|
render_diagram("uml", "A -> B", cache_dir=str(tmp_path))
|
||||||
|
flush.assert_not_called()
|
||||||
|
reset_diagram_batch()
|
||||||
|
|
||||||
|
|
||||||
|
def test_diagram_figure_registers_for_batch():
|
||||||
|
from docx import Document
|
||||||
|
from md2gost import package_dir
|
||||||
|
import md2gost.diagram_renderer as dr
|
||||||
|
import os
|
||||||
|
|
||||||
|
reset_diagram_batch()
|
||||||
|
doc = Document(os.path.join(package_dir(), "Template.docx"))
|
||||||
|
DiagramFigure(doc._body, "uml", "A -> B")
|
||||||
|
DiagramFigure(doc._body, "uml", "A -> B")
|
||||||
|
DiagramFigure(doc._body, "mermaid", "flowchart LR\nA-->B")
|
||||||
|
assert dr._PENDING == [("uml", "A -> B"), ("mermaid", "flowchart LR\nA-->B")]
|
||||||
|
reset_diagram_batch()
|
||||||
|
assert dr._PENDING == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_plantuml_cache_key_carries_jar_version(tmp_path):
|
||||||
|
assert PLANTUML_VERSION in PLANTUML_CACHE_PREFIX
|
||||||
|
prepared, dtype = prepare_source("uml", "A -> B")
|
||||||
|
assert dtype == "plantuml"
|
||||||
|
versioned, _ = _cache_paths(
|
||||||
|
prepared, str(tmp_path), cache_prefix=PLANTUML_CACHE_PREFIX,
|
||||||
|
)
|
||||||
|
legacy, _ = _cache_paths(prepared, str(tmp_path))
|
||||||
|
assert versioned != legacy
|
||||||
|
|
||||||
|
|
||||||
|
def test_stale_unversioned_cache_is_a_miss_not_a_crash(tmp_path):
|
||||||
|
"""Files cached before the jar version entered the key must be re-rendered."""
|
||||||
|
configure_diagrams(fallback="remote", cache_dir=str(tmp_path), diagram_scale=1)
|
||||||
|
prepared, _ = prepare_source("uml", "A -> B")
|
||||||
|
legacy, _ = _cache_paths(prepared, str(tmp_path))
|
||||||
|
legacy.write_bytes(b"\x89PNG\r\n\x1a\n" + b"stale")
|
||||||
|
|
||||||
|
def fake_kroki(source, diagram_type, base_url, out_path, fmt="png"):
|
||||||
|
out_path.write_bytes(b"\x89PNG\r\n\x1a\n" + b"fresh")
|
||||||
|
return True
|
||||||
|
|
||||||
|
with patch("md2gost.diagram_renderer._render_plantuml_jar", return_value=False), \
|
||||||
|
patch("md2gost.diagram_renderer._render_kroki", side_effect=fake_kroki):
|
||||||
|
result = render_diagram(
|
||||||
|
"uml", "A -> B", cache_dir=str(tmp_path), diagram_scale=1,
|
||||||
|
)
|
||||||
|
assert Path(result.png_path) != legacy
|
||||||
|
assert Path(result.png_path).read_bytes().endswith(b"fresh")
|
||||||
|
assert legacy.read_bytes().endswith(b"stale")
|
||||||
|
|
||||||
|
|
||||||
def test_resolve_plantuml_jar_explicit(tmp_path):
|
def test_resolve_plantuml_jar_explicit(tmp_path):
|
||||||
jar = tmp_path / "plantuml.jar"
|
jar = tmp_path / "plantuml.jar"
|
||||||
jar.write_bytes(b"PK" + b"\x00" * 2000)
|
jar.write_bytes(b"PK" + b"\x00" * 2000)
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
"""Пустой заголовок: конвертер не падает, но и не молчит.
|
||||||
|
|
||||||
|
Методичка МИРЭА (`_method_extract.txt`, строка 156): «…разделы, пункты …
|
||||||
|
имеют заголовки, четко и кратко отражающие их содержание». Заголовок без
|
||||||
|
текста — нарушение, а не валидный ввод: он даст в документе пронумерованный,
|
||||||
|
но безымянный раздел, который попадёт в автособираемое содержание. Значит,
|
||||||
|
парсер обязан такую строку пережить, а checker — сообщить о ней.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from md2gost.checker import check_markdown
|
||||||
|
from md2gost.extended_markdown.heading import Heading
|
||||||
|
|
||||||
|
|
||||||
|
MD_EMPTY_HEADING = """# *СОДЕРЖАНИЕ
|
||||||
|
[TOC]
|
||||||
|
|
||||||
|
#
|
||||||
|
|
||||||
|
Текст раздела без заголовка.
|
||||||
|
"""
|
||||||
|
|
||||||
|
MD_OK_HEADINGS = """# *СОДЕРЖАНИЕ
|
||||||
|
[TOC]
|
||||||
|
|
||||||
|
# Анализ предметной области
|
||||||
|
|
||||||
|
Текст раздела.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _heading(source: str) -> Heading:
|
||||||
|
match = Heading.pattern.match(source)
|
||||||
|
assert match is not None, f"pattern did not match {source!r}"
|
||||||
|
return Heading(match)
|
||||||
|
|
||||||
|
|
||||||
|
class TestEmptyHeadingParsing(unittest.TestCase):
|
||||||
|
def test_bare_hash_does_not_raise(self):
|
||||||
|
heading = _heading("#\n")
|
||||||
|
|
||||||
|
self.assertEqual("", heading.inline_body)
|
||||||
|
self.assertTrue(heading.numbered)
|
||||||
|
self.assertEqual(1, heading.level)
|
||||||
|
|
||||||
|
def test_bare_hashes_of_deeper_level(self):
|
||||||
|
heading = _heading("###\n")
|
||||||
|
|
||||||
|
self.assertEqual("", heading.inline_body)
|
||||||
|
self.assertTrue(heading.numbered)
|
||||||
|
self.assertEqual(3, heading.level)
|
||||||
|
|
||||||
|
def test_asterisk_without_text_is_unnumbered(self):
|
||||||
|
heading = _heading("# *\n")
|
||||||
|
|
||||||
|
self.assertEqual("", heading.inline_body)
|
||||||
|
self.assertFalse(heading.numbered)
|
||||||
|
|
||||||
|
def test_normal_headings_are_unaffected(self):
|
||||||
|
numbered = _heading("# Анализ предметной области\n")
|
||||||
|
self.assertEqual("Анализ предметной области", numbered.inline_body)
|
||||||
|
self.assertTrue(numbered.numbered)
|
||||||
|
|
||||||
|
unnumbered = _heading("## *ВВЕДЕНИЕ\n")
|
||||||
|
self.assertEqual("ВВЕДЕНИЕ", unnumbered.inline_body)
|
||||||
|
self.assertFalse(unnumbered.numbered)
|
||||||
|
|
||||||
|
|
||||||
|
class TestEmptyHeadingChecker(unittest.TestCase):
|
||||||
|
def test_empty_heading_is_reported(self):
|
||||||
|
issues = check_markdown(MD_EMPTY_HEADING, "coursework")
|
||||||
|
|
||||||
|
found = [i for i in issues if i.id == "heading.empty"]
|
||||||
|
self.assertEqual(1, len(found))
|
||||||
|
self.assertEqual("warning", found[0].severity)
|
||||||
|
self.assertEqual(4, found[0].line)
|
||||||
|
|
||||||
|
def test_normal_headings_produce_no_issue(self):
|
||||||
|
issues = check_markdown(MD_OK_HEADINGS, "coursework")
|
||||||
|
|
||||||
|
self.assertEqual([], [i for i in issues if i.id == "heading.empty"])
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
"""The mml2omml stylesheet is now compiled once and shared between formulas.
|
||||||
|
|
||||||
|
These tests pin the two properties that makes safe: the output must match a
|
||||||
|
freshly compiled transform, and reusing the compiled object must not let one
|
||||||
|
formula affect the next.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import latex2mathml.converter
|
||||||
|
from lxml import etree
|
||||||
|
|
||||||
|
from md2gost import package_dir
|
||||||
|
from md2gost.latex_math import latex_to_omml
|
||||||
|
|
||||||
|
_FORMULAS = [
|
||||||
|
"x^2 + y^2 = z^2",
|
||||||
|
r"\frac{a}{b}",
|
||||||
|
r"\frac{a + b}{c + d}",
|
||||||
|
r"\sum_{i=1}^{n} i",
|
||||||
|
r"\sqrt{x+1}",
|
||||||
|
r"\int_0^1 f(x)dx",
|
||||||
|
r"\alpha \beta \gamma",
|
||||||
|
r"E = mc^2",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _freshly_compiled(latex_equation: str) -> bytes:
|
||||||
|
"""Pre-optimization behaviour: parse and compile the stylesheet per call."""
|
||||||
|
mathml = latex2mathml.converter.convert(latex_equation)
|
||||||
|
tree = etree.fromstring(mathml)
|
||||||
|
transform = etree.XSLT(etree.parse(os.path.join(package_dir(), "mml2omml")))
|
||||||
|
return etree.tostring(transform(tree).getroot())
|
||||||
|
|
||||||
|
|
||||||
|
class TestLatexToOmml(unittest.TestCase):
|
||||||
|
def test_matches_freshly_compiled_transform(self):
|
||||||
|
for formula in _FORMULAS:
|
||||||
|
with self.subTest(formula=formula):
|
||||||
|
self.assertEqual(_freshly_compiled(formula),
|
||||||
|
etree.tostring(latex_to_omml(formula)))
|
||||||
|
|
||||||
|
def test_repeated_calls_are_identical(self):
|
||||||
|
for formula in _FORMULAS:
|
||||||
|
with self.subTest(formula=formula):
|
||||||
|
outputs = {etree.tostring(latex_to_omml(formula)) for _ in range(5)}
|
||||||
|
self.assertEqual(1, len(outputs))
|
||||||
|
|
||||||
|
def test_interleaved_formulas_do_not_interfere(self):
|
||||||
|
expected = {f: etree.tostring(latex_to_omml(f)) for f in _FORMULAS}
|
||||||
|
for _ in range(3):
|
||||||
|
for formula in reversed(_FORMULAS):
|
||||||
|
self.assertEqual(expected[formula], etree.tostring(latex_to_omml(formula)))
|
||||||
|
|
||||||
|
def test_invalid_formula_still_raises_value_error(self):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
latex_to_omml(r"\frac{")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
"""Дефис внутри гиперссылки: текст не должен дублироваться.
|
||||||
|
|
||||||
|
Методичка МИРЭА (`_method_extract.txt`, строки 373-376): знак дефиса «-» не
|
||||||
|
отделяется пробелами и используется в сложных словах (во-первых,
|
||||||
|
физико-математический, кто-либо) и в интервалах (100-150 Мб). Поэтому слово не
|
||||||
|
должно рваться по дефису при переносе строки — вместо обычного дефиса в текст
|
||||||
|
пишется отдельный элемент `w:noBreakHyphen`, как это уже делает
|
||||||
|
`Paragraph.add_run`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import docx
|
||||||
|
from docx.oxml.ns import qn
|
||||||
|
|
||||||
|
from md2gost import package_dir
|
||||||
|
from md2gost.renderable.paragraph import Link, Paragraph
|
||||||
|
from md2gost.styles import apply_mirea_styles
|
||||||
|
|
||||||
|
|
||||||
|
def _texts(element) -> list[str]:
|
||||||
|
return [t.text or "" for t in element.iter(qn("w:t"))]
|
||||||
|
|
||||||
|
|
||||||
|
class TestLinkHyphen(unittest.TestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self._document = docx.Document(os.path.join(package_dir(), "Template.docx"))
|
||||||
|
self._document._body.clear_content()
|
||||||
|
apply_mirea_styles(self._document)
|
||||||
|
|
||||||
|
def _hyperlink(self, text: str):
|
||||||
|
paragraph = Paragraph(self._document._body)
|
||||||
|
link = Link("https://example.com", paragraph._docx_paragraph)
|
||||||
|
link.add_run(text)
|
||||||
|
return link.element
|
||||||
|
|
||||||
|
def test_compound_word_is_split_around_no_break_hyphen(self):
|
||||||
|
hyperlink = self._hyperlink("во-первых")
|
||||||
|
|
||||||
|
runs = hyperlink.findall(qn("w:r"))
|
||||||
|
self.assertEqual(3, len(runs))
|
||||||
|
self.assertEqual(
|
||||||
|
1, len(hyperlink.findall(f'{qn("w:r")}/{qn("w:noBreakHyphen")}'))
|
||||||
|
)
|
||||||
|
self.assertEqual("вопервых", "".join(_texts(hyperlink)))
|
||||||
|
for run in runs:
|
||||||
|
self.assertNotIn("во-первых", "".join(_texts(run)))
|
||||||
|
|
||||||
|
def test_interval_is_split_around_no_break_hyphen(self):
|
||||||
|
hyperlink = self._hyperlink("100-150")
|
||||||
|
|
||||||
|
self.assertEqual(["100", "150"], [t for t in _texts(hyperlink) if t])
|
||||||
|
|
||||||
|
def test_several_hyphens_produce_one_run_per_part(self):
|
||||||
|
hyperlink = self._hyperlink("физико-математический-факультет")
|
||||||
|
|
||||||
|
self.assertEqual(5, len(hyperlink.findall(qn("w:r"))))
|
||||||
|
self.assertEqual(
|
||||||
|
"физикоматематическийфакультет", "".join(_texts(hyperlink))
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_text_without_hyphen_stays_one_run(self):
|
||||||
|
hyperlink = self._hyperlink("пример")
|
||||||
|
|
||||||
|
self.assertEqual(1, len(hyperlink.findall(qn("w:r"))))
|
||||||
|
self.assertEqual("пример", "".join(_texts(hyperlink)))
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
"""Оформление листинга и флаг подсветки синтаксиса.
|
||||||
|
|
||||||
|
Методичка МИРЭА, табл. 4.1 (`_method_extract.txt`, строки 716-737), блок
|
||||||
|
«Содержание листинга»: шрифт Courier New, начертание Обычный, размер 10 пт,
|
||||||
|
цвет текста Авто (чёрный), выравнивание по левому краю, отступы слева/справа
|
||||||
|
0 см, первая строка 0 см, интервал перед и после 0 мм, междустрочный
|
||||||
|
одинарный.
|
||||||
|
|
||||||
|
Отсюда два независимых требования:
|
||||||
|
|
||||||
|
* с выключенной подсветкой (дефолт) содержимое листинга не должно нести
|
||||||
|
никакого прямого форматирования на уровне run'а;
|
||||||
|
* если пользователь всё же включил `--syntax-highlighting`, флаг обязан
|
||||||
|
работать честно — курсив из стиля Pygments должен доезжать до XML, а checker
|
||||||
|
обязан предупредить об отклонении от табл. 4.1.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
import docx
|
||||||
|
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING
|
||||||
|
from docx.oxml.ns import qn
|
||||||
|
from docx.shared import Pt
|
||||||
|
|
||||||
|
from md2gost import package_dir
|
||||||
|
from md2gost.checker import check_markdown
|
||||||
|
from md2gost.renderable.caption import CaptionInfo
|
||||||
|
from md2gost.renderable.listing import Listing
|
||||||
|
from md2gost.styles import apply_mirea_styles
|
||||||
|
|
||||||
|
|
||||||
|
CODE = '# комментарий\nprint("hello")\n'
|
||||||
|
|
||||||
|
MD_WITH_LISTING = """# *СОДЕРЖАНИЕ
|
||||||
|
[TOC]
|
||||||
|
|
||||||
|
# Раздел
|
||||||
|
|
||||||
|
%code1 Пример кода
|
||||||
|
|
||||||
|
```python
|
||||||
|
print(1)
|
||||||
|
```
|
||||||
|
|
||||||
|
См. @Листинг:code1.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def _syntax_highlighting(enabled: bool):
|
||||||
|
previous = os.environ.get("SYNTAX_HIGHLIGHTING")
|
||||||
|
if enabled:
|
||||||
|
os.environ["SYNTAX_HIGHLIGHTING"] = "1"
|
||||||
|
else:
|
||||||
|
os.environ.pop("SYNTAX_HIGHLIGHTING", None)
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
if previous is None:
|
||||||
|
os.environ.pop("SYNTAX_HIGHLIGHTING", None)
|
||||||
|
else:
|
||||||
|
os.environ["SYNTAX_HIGHLIGHTING"] = previous
|
||||||
|
|
||||||
|
|
||||||
|
class _ListingTestCase(unittest.TestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self._document = docx.Document(os.path.join(package_dir(), "Template.docx"))
|
||||||
|
self._document._body.clear_content()
|
||||||
|
apply_mirea_styles(self._document)
|
||||||
|
|
||||||
|
def _listing(self, highlighting: bool) -> Listing:
|
||||||
|
listing = Listing(self._document._body, "python", CaptionInfo("code1", "Пример"))
|
||||||
|
with _syntax_highlighting(highlighting):
|
||||||
|
listing.set_text(CODE)
|
||||||
|
return listing
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _runs(listing: Listing):
|
||||||
|
for paragraph in listing.paragraphs:
|
||||||
|
for run in paragraph._docx_paragraph.runs:
|
||||||
|
yield run
|
||||||
|
|
||||||
|
|
||||||
|
class TestListingWithoutHighlighting(_ListingTestCase):
|
||||||
|
"""Табл. 4.1: содержимое листинга — Обычный, чёрный, Courier New 10 пт."""
|
||||||
|
|
||||||
|
def test_runs_carry_no_direct_formatting(self):
|
||||||
|
listing = self._listing(highlighting=False)
|
||||||
|
|
||||||
|
runs = list(self._runs(listing))
|
||||||
|
self.assertTrue(runs)
|
||||||
|
for run in runs:
|
||||||
|
rPr = run._element.find(qn("w:rPr"))
|
||||||
|
if rPr is None:
|
||||||
|
continue
|
||||||
|
self.assertIsNone(rPr.find(qn("w:b")), run.text)
|
||||||
|
self.assertIsNone(rPr.find(qn("w:i")), run.text)
|
||||||
|
self.assertIsNone(rPr.find(qn("w:color")), run.text)
|
||||||
|
|
||||||
|
def test_code_style_matches_table_4_1(self):
|
||||||
|
style = self._document.styles["Code"]
|
||||||
|
|
||||||
|
self.assertEqual("Courier New", style.font.name)
|
||||||
|
self.assertEqual(Pt(10), style.font.size)
|
||||||
|
self.assertFalse(style.font.bold)
|
||||||
|
self.assertFalse(style.font.italic)
|
||||||
|
|
||||||
|
pf = style.paragraph_format
|
||||||
|
self.assertEqual(WD_ALIGN_PARAGRAPH.LEFT, pf.alignment)
|
||||||
|
self.assertAlmostEqual(0, pf.first_line_indent.cm, places=3)
|
||||||
|
self.assertAlmostEqual(0, pf.left_indent.cm, places=3)
|
||||||
|
self.assertAlmostEqual(0, pf.space_before.mm, places=3)
|
||||||
|
self.assertAlmostEqual(0, pf.space_after.mm, places=3)
|
||||||
|
self.assertEqual(WD_LINE_SPACING.SINGLE, pf.line_spacing_rule)
|
||||||
|
|
||||||
|
|
||||||
|
class TestListingWithHighlighting(_ListingTestCase):
|
||||||
|
"""Включённый флаг обязан реально давать курсив там, где его задаёт Pygments."""
|
||||||
|
|
||||||
|
def test_italic_token_reaches_xml(self):
|
||||||
|
listing = self._listing(highlighting=True)
|
||||||
|
|
||||||
|
italic_runs = [run for run in self._runs(listing) if run.italic]
|
||||||
|
self.assertTrue(
|
||||||
|
italic_runs,
|
||||||
|
"ни один run не получил курсив, хотя стиль Pygments задаёт italic "
|
||||||
|
"для Token.Comment",
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
any("комментарий" in run.text for run in italic_runs),
|
||||||
|
[run.text for run in italic_runs],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSyntaxHighlightingChecker(unittest.TestCase):
|
||||||
|
"""Подсветка противоречит табл. 4.1 — checker обязан об этом сказать."""
|
||||||
|
|
||||||
|
def test_no_issue_when_flag_is_off(self):
|
||||||
|
issues = check_markdown(MD_WITH_LISTING, "coursework")
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
[], [i for i in issues if i.id == "listing.syntax_highlighting"]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_warning_when_flag_is_on(self):
|
||||||
|
issues = check_markdown(
|
||||||
|
MD_WITH_LISTING, "coursework", syntax_highlighting=True
|
||||||
|
)
|
||||||
|
|
||||||
|
found = [i for i in issues if i.id == "listing.syntax_highlighting"]
|
||||||
|
self.assertEqual(1, len(found))
|
||||||
|
self.assertEqual("warning", found[0].severity)
|
||||||
|
|
||||||
|
def test_error_under_strict(self):
|
||||||
|
issues = check_markdown(
|
||||||
|
MD_WITH_LISTING, "coursework", syntax_highlighting=True, strict=True
|
||||||
|
)
|
||||||
|
|
||||||
|
found = [i for i in issues if i.id == "listing.syntax_highlighting"]
|
||||||
|
self.assertEqual(1, len(found))
|
||||||
|
self.assertEqual("error", found[0].severity)
|
||||||
@@ -226,7 +226,7 @@ class TestParagraphSizer(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(1, ps.count_lines(paragraph.runs, self._max_width - LISTING_OFFSET, paragraph.style.font, 0, True))
|
self.assertEqual(1, ps.count_lines(paragraph.runs, self._max_width - LISTING_OFFSET, paragraph.style.font, 0, True))
|
||||||
|
|
||||||
def test_count_lines_courier7(self):
|
def test_count_lines_courier7_2(self):
|
||||||
paragraph = self._document.add_paragraph(style="Code")
|
paragraph = self._document.add_paragraph(style="Code")
|
||||||
paragraph.add_run(
|
paragraph.add_run(
|
||||||
""" # and ((min(2, height_data.lines) - 1) * height_data.line_spacing + 1) * height_data.line_height\\""")
|
""" # and ((min(2, height_data.lines) - 1) * height_data.line_spacing + 1) * height_data.line_height\\""")
|
||||||
|
|||||||
@@ -0,0 +1,462 @@
|
|||||||
|
"""Equivalence tests for the ParagraphSizer hot-path optimizations.
|
||||||
|
|
||||||
|
`_LegacyFont` and `_legacy_count_lines` are verbatim copies of the
|
||||||
|
pre-optimization implementations. Every test here asserts that the current
|
||||||
|
implementation produces bit-identical results, so the caching and the
|
||||||
|
per-word (instead of per-character) width measurement cannot silently move
|
||||||
|
line breaks.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from functools import cached_property
|
||||||
|
from math import ceil
|
||||||
|
|
||||||
|
from docx.oxml import OxmlElement
|
||||||
|
from docx.shared import Cm, Pt
|
||||||
|
from docx.text.font import Font as DocxFont
|
||||||
|
from docx.text.run import Run
|
||||||
|
from freetype import Face
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
|
from md2gost.renderable.find_font import find_font
|
||||||
|
from md2gost.renderable.listing import LISTING_OFFSET
|
||||||
|
from md2gost.renderable.paragraph_sizer import (
|
||||||
|
Font,
|
||||||
|
ParagraphSizer,
|
||||||
|
_get_font,
|
||||||
|
_merge_objects,
|
||||||
|
measure_paragraph,
|
||||||
|
)
|
||||||
|
|
||||||
|
from . import _create_test_document
|
||||||
|
|
||||||
|
|
||||||
|
class _LegacyFont:
|
||||||
|
"""Verbatim copy of Font before the metrics snapshot was introduced."""
|
||||||
|
|
||||||
|
def __init__(self, name: str, bold: bool, italic: bool, size_pt: int):
|
||||||
|
path = find_font(name, bold, italic)
|
||||||
|
self._freetypefont = ImageFont.truetype(path, size_pt)
|
||||||
|
self._draw = ImageDraw.Draw(Image.new("RGB", (1000, 1000)))
|
||||||
|
|
||||||
|
self._face = Face(path)
|
||||||
|
self._face.set_char_size(int(size_pt * 64))
|
||||||
|
|
||||||
|
def get_text_width(self, text: str):
|
||||||
|
if not self.is_mono:
|
||||||
|
bbox = self._draw.textbbox((0, 0), text, self._freetypefont)
|
||||||
|
return Pt(bbox[2] - bbox[0])
|
||||||
|
else:
|
||||||
|
return Pt(len(text) * self._face.glyph.advance.x / 64)
|
||||||
|
|
||||||
|
def get_line_height(self):
|
||||||
|
if "Times" in str(self._face.family_name) and self._freetypefont.size == 14:
|
||||||
|
return Pt(16.05)
|
||||||
|
if "Courier" in str(self._face.family_name) and self._freetypefont.size == 12:
|
||||||
|
return Pt(13.61)
|
||||||
|
else:
|
||||||
|
return Pt(self._face.size.height / 64)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def is_mono(self):
|
||||||
|
self._face.load_char("i")
|
||||||
|
i_width = self._face.glyph.advance.x
|
||||||
|
self._face.load_char("m")
|
||||||
|
return i_width == self._face.glyph.advance.x
|
||||||
|
|
||||||
|
|
||||||
|
def _legacy_count_lines(runs, max_width, docx_font: DocxFont, first_line_indent, is_mono: bool = False):
|
||||||
|
"""Verbatim copy of ParagraphSizer.count_lines before the optimization."""
|
||||||
|
lines = 1
|
||||||
|
line_width = first_line_indent
|
||||||
|
|
||||||
|
space_width = _LegacyFont(
|
||||||
|
docx_font.name, docx_font.bold, docx_font.italic, docx_font.size.pt).get_text_width(" ")
|
||||||
|
if not is_mono:
|
||||||
|
space_width *= 0.81
|
||||||
|
|
||||||
|
word_part = ""
|
||||||
|
word_parts_widths = [0]
|
||||||
|
spaces = 0
|
||||||
|
for i, run in enumerate(runs):
|
||||||
|
if word_part:
|
||||||
|
word_part = ""
|
||||||
|
word_parts_widths.append(0)
|
||||||
|
|
||||||
|
run_docx_font = _merge_objects(docx_font, run.font)
|
||||||
|
font = _LegacyFont(run_docx_font.name, run_docx_font.bold,
|
||||||
|
run_docx_font.italic, run_docx_font.size.pt)
|
||||||
|
|
||||||
|
run_text = run.text
|
||||||
|
if run_text == "" and run._element.xpath("w:noBreakHyphen"):
|
||||||
|
run_text = "-"
|
||||||
|
if i == len(runs) - 1:
|
||||||
|
run_text += " "
|
||||||
|
|
||||||
|
for c in run_text:
|
||||||
|
if c == " ":
|
||||||
|
if any(word_parts_widths):
|
||||||
|
width = spaces * space_width + sum(word_parts_widths)
|
||||||
|
if width <= max_width - line_width:
|
||||||
|
line_width += width
|
||||||
|
elif width > max_width - first_line_indent:
|
||||||
|
if lines == 1 and line_width == first_line_indent and not spaces:
|
||||||
|
lines += ceil((width - (max_width - first_line_indent)) / max_width)
|
||||||
|
line_width = (width - (max_width - first_line_indent)) % max_width
|
||||||
|
else:
|
||||||
|
lines += ceil(width / max_width)
|
||||||
|
line_width = width % max_width
|
||||||
|
else:
|
||||||
|
lines += 1
|
||||||
|
line_width = sum(word_parts_widths)
|
||||||
|
|
||||||
|
word_part = ""
|
||||||
|
word_parts_widths = [0]
|
||||||
|
spaces = 1
|
||||||
|
else:
|
||||||
|
spaces += 1
|
||||||
|
else:
|
||||||
|
word_part += c
|
||||||
|
word_parts_widths[-1] = font.get_text_width(word_part)
|
||||||
|
|
||||||
|
return int(lines)
|
||||||
|
|
||||||
|
|
||||||
|
_BODY = (
|
||||||
|
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam lacinia fringilla lectus, "
|
||||||
|
"nec euismod odio convallis sed. Nunc ac libero ultricies, condimentum neque et, "
|
||||||
|
"fermentum urna. Donec feugiat diam sed nulla rutrum, sit amet accumsan odio tempor."
|
||||||
|
)
|
||||||
|
|
||||||
|
# (name, list of run texts). Single-element lists are single-run paragraphs.
|
||||||
|
_SINGLE_RUN_CASES = [
|
||||||
|
("body", _BODY),
|
||||||
|
("empty", ""),
|
||||||
|
("one_space", " "),
|
||||||
|
("only_spaces", " "),
|
||||||
|
("one_word", "word"),
|
||||||
|
("leading_spaces", " leading spaces after indent"),
|
||||||
|
("trailing_spaces", "trailing spaces before end "),
|
||||||
|
("double_spaces", "double spaces between words"),
|
||||||
|
("long_word", "verylongword" * 13),
|
||||||
|
("word_then_long_word", "someword " + "verylongword" * 13),
|
||||||
|
("long_word_then_word", "verylongword" * 13 + " someword"),
|
||||||
|
("cyrillic", "Электроэнцефалографический анализ показал устойчивую динамику показателей."),
|
||||||
|
("cyrillic_long", "Электроэнцефалографический " * 8),
|
||||||
|
("punctuation_only", ". , ; : ! ?"),
|
||||||
|
("em_dash_tail", "OpenAI is a leading research organization. Hello world —"),
|
||||||
|
("code_line", " table._cells[0]._element.append(paragraph_rendered_info.docx_element._element)"),
|
||||||
|
("code_short", " continuation_paragraph = Paragraph(self.parent)"),
|
||||||
|
("code_ooo", "ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo"),
|
||||||
|
("code_comment", " # and ((min(2, height_data.lines) - 1) * height_data.line_spacing + 1)\\"),
|
||||||
|
("tabs_as_spaces", " def add_link(self, text: str, url: str, is_bold: bool = None):"),
|
||||||
|
]
|
||||||
|
|
||||||
|
_MULTI_RUN_CASES = [
|
||||||
|
("split_mid_word", ["verylong", "wordthat", "spansruns and then more text follows here"]),
|
||||||
|
("split_mid_word_long", ["verylongword" * 6, "verylongword" * 7]),
|
||||||
|
("empty_runs_interleaved", ["", "hello", "", " ", "", "world", ""]),
|
||||||
|
("space_owned_by_own_run", ["hello", " ", "world", " ", "again"]),
|
||||||
|
("code_tokens", ["", " ", "run", " ", "=", " ", "DocxRun", "(", "create_element",
|
||||||
|
"(", '"', "w:r", '"', ")", ",", " ", "self", ".", "_docx_paragraph", ")"]),
|
||||||
|
("code_tokens2", ["", " ", "-", ">", " ", "Generator", "[", "RenderedInfo", " ",
|
||||||
|
"|", " ", "Renderable", ",", " ", "None", ",", " ", "None", "]", ":"]),
|
||||||
|
("trailing_empty_run", ["some words here", ""]),
|
||||||
|
("trailing_space_run", ["some words here", " "]),
|
||||||
|
("all_empty", ["", "", ""]),
|
||||||
|
("word_ends_exactly_at_run_end", ["complete", " ", "words", " ", "only"]),
|
||||||
|
("many_fragments_one_word", list("supercalifragilisticexpialidocious")),
|
||||||
|
("long_text_split_in_three", [_BODY[:90], _BODY[90:180], _BODY[180:]]),
|
||||||
|
]
|
||||||
|
|
||||||
|
# (bold, italic) per run, cycled - exercises the per-run font merge path
|
||||||
|
_STYLE_CYCLE = [(None, None), (True, None), (None, True), (True, True)]
|
||||||
|
|
||||||
|
|
||||||
|
class TestCountLinesEquivalence(unittest.TestCase):
|
||||||
|
"""New count_lines must return exactly what the legacy implementation returned."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self._document, self._max_height, self._max_width = _create_test_document()
|
||||||
|
|
||||||
|
def _assert_same(self, paragraph, max_width, first_line_indent, is_mono):
|
||||||
|
sizer = ParagraphSizer(paragraph, None, max_width)
|
||||||
|
runs = paragraph.runs
|
||||||
|
docx_font = paragraph.style.font
|
||||||
|
expected = _legacy_count_lines(runs, max_width, docx_font, first_line_indent, is_mono)
|
||||||
|
actual = sizer.count_lines(runs, max_width, docx_font, first_line_indent, is_mono)
|
||||||
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
|
def test_single_run_normal_style(self):
|
||||||
|
for name, text in _SINGLE_RUN_CASES:
|
||||||
|
for indent in (0, Cm(1.25)):
|
||||||
|
with self.subTest(case=name, indent=indent):
|
||||||
|
paragraph = self._document.add_paragraph()
|
||||||
|
paragraph.add_run(text)
|
||||||
|
self._assert_same(paragraph, self._max_width, indent, False)
|
||||||
|
|
||||||
|
def test_single_run_code_style(self):
|
||||||
|
max_width = self._max_width - LISTING_OFFSET
|
||||||
|
for name, text in _SINGLE_RUN_CASES:
|
||||||
|
for is_mono in (False, True):
|
||||||
|
with self.subTest(case=name, is_mono=is_mono):
|
||||||
|
paragraph = self._document.add_paragraph(style="Code")
|
||||||
|
paragraph.add_run(text)
|
||||||
|
self._assert_same(paragraph, max_width, 0, is_mono)
|
||||||
|
|
||||||
|
def test_single_run_narrow_width(self):
|
||||||
|
"""Narrow columns push almost every word onto its own line."""
|
||||||
|
for name, text in _SINGLE_RUN_CASES:
|
||||||
|
with self.subTest(case=name):
|
||||||
|
paragraph = self._document.add_paragraph()
|
||||||
|
paragraph.add_run(text)
|
||||||
|
self._assert_same(paragraph, Cm(3), 0, False)
|
||||||
|
|
||||||
|
def test_multi_run_normal_style(self):
|
||||||
|
for name, texts in _MULTI_RUN_CASES:
|
||||||
|
for indent in (0, Cm(1.25)):
|
||||||
|
with self.subTest(case=name, indent=indent):
|
||||||
|
paragraph = self._document.add_paragraph()
|
||||||
|
for text in texts:
|
||||||
|
paragraph.add_run(text)
|
||||||
|
self._assert_same(paragraph, self._max_width, indent, False)
|
||||||
|
|
||||||
|
def test_multi_run_code_style(self):
|
||||||
|
max_width = self._max_width - LISTING_OFFSET
|
||||||
|
for name, texts in _MULTI_RUN_CASES:
|
||||||
|
with self.subTest(case=name):
|
||||||
|
paragraph = self._document.add_paragraph(style="Code")
|
||||||
|
for text in texts:
|
||||||
|
paragraph.add_run(text)
|
||||||
|
self._assert_same(paragraph, max_width, 0, True)
|
||||||
|
|
||||||
|
def test_multi_run_mixed_bold_italic(self):
|
||||||
|
"""Different fonts per run: word fragments must be measured with their own font."""
|
||||||
|
for name, texts in _MULTI_RUN_CASES:
|
||||||
|
with self.subTest(case=name):
|
||||||
|
paragraph = self._document.add_paragraph()
|
||||||
|
for i, text in enumerate(texts):
|
||||||
|
run = paragraph.add_run(text)
|
||||||
|
run.font.bold, run.font.italic = _STYLE_CYCLE[i % len(_STYLE_CYCLE)]
|
||||||
|
self._assert_same(paragraph, self._max_width, Cm(1.25), False)
|
||||||
|
|
||||||
|
def test_no_break_hyphen_runs(self):
|
||||||
|
"""An empty run carrying w:noBreakHyphen is measured as a literal '-'."""
|
||||||
|
for texts in (["во", "", "первых"],
|
||||||
|
["физико", "", "математический анализ данных"],
|
||||||
|
["", "", ""],
|
||||||
|
["интервал 100", "", "150 Мб и далее по тексту"]):
|
||||||
|
with self.subTest(texts=texts):
|
||||||
|
paragraph = self._document.add_paragraph()
|
||||||
|
for text in texts:
|
||||||
|
run = paragraph.add_run(text)
|
||||||
|
if text == "":
|
||||||
|
run._element.append(OxmlElement("w:noBreakHyphen"))
|
||||||
|
self._assert_same(paragraph, self._max_width, Cm(1.25), False)
|
||||||
|
|
||||||
|
def test_widths_measured_once_per_word(self):
|
||||||
|
"""Guards the optimization itself: no per-character re-measurement."""
|
||||||
|
paragraph = self._document.add_paragraph()
|
||||||
|
text = "alpha bravo charlie delta echo foxtrot"
|
||||||
|
paragraph.add_run(text)
|
||||||
|
|
||||||
|
calls = []
|
||||||
|
original = Font.get_text_width
|
||||||
|
|
||||||
|
def counting(self, value):
|
||||||
|
calls.append(value)
|
||||||
|
return original(self, value)
|
||||||
|
|
||||||
|
Font.get_text_width = counting
|
||||||
|
try:
|
||||||
|
ParagraphSizer(paragraph, None, self._max_width).count_lines(
|
||||||
|
paragraph.runs, self._max_width, paragraph.style.font, Cm(1.25))
|
||||||
|
finally:
|
||||||
|
Font.get_text_width = original
|
||||||
|
|
||||||
|
words = text.split(" ")
|
||||||
|
# one call for the space width, then one per whole word
|
||||||
|
self.assertEqual([" "] + words, calls)
|
||||||
|
self.assertLess(len(calls), len(text.replace(" ", "")))
|
||||||
|
|
||||||
|
|
||||||
|
class TestCalculateHeightEquivalence(unittest.TestCase):
|
||||||
|
"""Full calculate_height results for a representative document must not move."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self._document, self._max_height, self._max_width = _create_test_document()
|
||||||
|
|
||||||
|
def _result_tuple(self, paragraph, max_width):
|
||||||
|
result = ParagraphSizer(paragraph, None, max_width).calculate_height()
|
||||||
|
return (result.lines, int(result.line_height), result.line_spacing,
|
||||||
|
int(result.before), int(result.after), int(result.base), int(result.full))
|
||||||
|
|
||||||
|
def test_repeated_calls_are_stable(self):
|
||||||
|
"""Shared Font instances must not accumulate state across calls."""
|
||||||
|
paragraph = self._document.add_paragraph()
|
||||||
|
paragraph.add_run(_BODY)
|
||||||
|
first = self._result_tuple(paragraph, self._max_width)
|
||||||
|
for _ in range(5):
|
||||||
|
self.assertEqual(first, self._result_tuple(paragraph, self._max_width))
|
||||||
|
|
||||||
|
def test_interleaved_fonts_do_not_interfere(self):
|
||||||
|
"""Alternating between cached fonts must not change either one's output."""
|
||||||
|
body = self._document.add_paragraph()
|
||||||
|
body.add_run(_BODY)
|
||||||
|
code = self._document.add_paragraph(style="Code")
|
||||||
|
code.add_run(" table._cells[0]._element.append(rendered_info.docx_element._element)")
|
||||||
|
|
||||||
|
body_first = self._result_tuple(body, self._max_width)
|
||||||
|
code_first = self._result_tuple(code, self._max_width - LISTING_OFFSET)
|
||||||
|
|
||||||
|
for _ in range(3):
|
||||||
|
self.assertEqual(code_first, self._result_tuple(code, self._max_width - LISTING_OFFSET))
|
||||||
|
self.assertEqual(body_first, self._result_tuple(body, self._max_width))
|
||||||
|
|
||||||
|
|
||||||
|
class TestFontCache(unittest.TestCase):
|
||||||
|
def test_same_descriptor_returns_same_instance(self):
|
||||||
|
self.assertIs(_get_font("Times New Roman", False, False, 14),
|
||||||
|
_get_font("Times New Roman", False, False, 14))
|
||||||
|
|
||||||
|
def test_different_descriptors_return_different_instances(self):
|
||||||
|
base = _get_font("Times New Roman", False, False, 14)
|
||||||
|
self.assertIsNot(base, _get_font("Times New Roman", True, False, 14))
|
||||||
|
self.assertIsNot(base, _get_font("Times New Roman", False, True, 14))
|
||||||
|
self.assertIsNot(base, _get_font("Times New Roman", False, False, 12))
|
||||||
|
self.assertIsNot(base, _get_font("Courier New", False, False, 14))
|
||||||
|
|
||||||
|
def test_cached_font_matches_fresh_font(self):
|
||||||
|
descriptors = [
|
||||||
|
("Times New Roman", False, False, 14),
|
||||||
|
("Times New Roman", True, False, 14),
|
||||||
|
("Times New Roman", False, True, 14),
|
||||||
|
("Times New Roman", True, True, 14),
|
||||||
|
("Courier New", False, False, 12),
|
||||||
|
("Calibri", False, False, 14),
|
||||||
|
]
|
||||||
|
samples = ["", " ", "i", "m", "hello", "in", "Электроэнцефалографический",
|
||||||
|
" ", "a b c", "verylongword" * 5, "()[]{}", "0123456789"]
|
||||||
|
for descriptor in descriptors:
|
||||||
|
legacy = _LegacyFont(*descriptor)
|
||||||
|
cached = _get_font(*descriptor)
|
||||||
|
with self.subTest(descriptor=descriptor):
|
||||||
|
self.assertEqual(legacy.is_mono, cached.is_mono)
|
||||||
|
self.assertEqual(legacy.get_line_height(), cached.get_line_height())
|
||||||
|
for sample in samples:
|
||||||
|
self.assertEqual(legacy.get_text_width(sample),
|
||||||
|
cached.get_text_width(sample),
|
||||||
|
msg=f"{descriptor} {sample!r}")
|
||||||
|
|
||||||
|
def test_mono_width_is_independent_of_glyph_slot(self):
|
||||||
|
"""get_text_width must not depend on whichever glyph was loaded last."""
|
||||||
|
font = _get_font("Courier New", False, False, 12)
|
||||||
|
self.assertTrue(font.is_mono)
|
||||||
|
expected = font.get_text_width("hello world")
|
||||||
|
for char in "iMW._@":
|
||||||
|
font._face.load_char(char)
|
||||||
|
self.assertEqual(expected, font.get_text_width("hello world"))
|
||||||
|
|
||||||
|
def test_font_holds_no_per_call_state(self):
|
||||||
|
"""Repeated and interleaved measurements return identical values."""
|
||||||
|
font = _get_font("Times New Roman", False, False, 14)
|
||||||
|
other = _get_font("Courier New", False, False, 12)
|
||||||
|
widths = {text: font.get_text_width(text) for text in ("a", "ab", "abc", "hello world")}
|
||||||
|
for _ in range(3):
|
||||||
|
other.get_text_width("hello world")
|
||||||
|
for text, width in widths.items():
|
||||||
|
self.assertEqual(width, font.get_text_width(text))
|
||||||
|
|
||||||
|
def test_public_font_constructor_still_works(self):
|
||||||
|
"""Font itself stays constructible - existing tests instantiate it directly."""
|
||||||
|
font = Font("Times New Roman", False, False, 14)
|
||||||
|
self.assertFalse(font.is_mono)
|
||||||
|
self.assertGreater(font.get_text_width("hello"), 0)
|
||||||
|
|
||||||
|
|
||||||
|
class TestMeasureParagraphCache(unittest.TestCase):
|
||||||
|
"""measure_paragraph() decides page breaks, so a hit must equal a fresh run."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self._document, self._max_height, self._max_width = _create_test_document()
|
||||||
|
|
||||||
|
def _fresh(self, paragraph, previous, max_width):
|
||||||
|
return ParagraphSizer(paragraph, previous, max_width).calculate_height()
|
||||||
|
|
||||||
|
def test_hit_equals_fresh_measurement(self):
|
||||||
|
cache = {}
|
||||||
|
paragraph = self._document.add_paragraph()
|
||||||
|
paragraph.add_run(_BODY)
|
||||||
|
first = measure_paragraph(cache, paragraph, None, self._max_width)
|
||||||
|
for _ in range(3):
|
||||||
|
self.assertEqual(self._fresh(paragraph, None, self._max_width), first)
|
||||||
|
self.assertEqual(first, measure_paragraph(cache, paragraph, None, self._max_width))
|
||||||
|
|
||||||
|
def test_edited_text_is_remeasured(self):
|
||||||
|
cache = {}
|
||||||
|
paragraph = self._document.add_paragraph()
|
||||||
|
run = paragraph.add_run("short")
|
||||||
|
short = measure_paragraph(cache, paragraph, None, self._max_width)
|
||||||
|
run.text = _BODY
|
||||||
|
long = measure_paragraph(cache, paragraph, None, self._max_width)
|
||||||
|
self.assertGreater(long.lines, short.lines)
|
||||||
|
self.assertEqual(self._fresh(paragraph, None, self._max_width), long)
|
||||||
|
|
||||||
|
def test_edited_spacing_is_remeasured(self):
|
||||||
|
cache = {}
|
||||||
|
paragraph = self._document.add_paragraph()
|
||||||
|
paragraph.add_run(_BODY)
|
||||||
|
before = measure_paragraph(cache, paragraph, None, self._max_width)
|
||||||
|
paragraph.paragraph_format.space_before = Pt(12)
|
||||||
|
after = measure_paragraph(cache, paragraph, None, self._max_width)
|
||||||
|
self.assertNotEqual(int(before.before), int(after.before))
|
||||||
|
self.assertEqual(self._fresh(paragraph, None, self._max_width), after)
|
||||||
|
|
||||||
|
def test_width_is_part_of_the_key(self):
|
||||||
|
cache = {}
|
||||||
|
paragraph = self._document.add_paragraph()
|
||||||
|
paragraph.add_run(_BODY)
|
||||||
|
wide = measure_paragraph(cache, paragraph, None, self._max_width)
|
||||||
|
narrow = measure_paragraph(cache, paragraph, None, self._max_width // 2)
|
||||||
|
self.assertGreater(narrow.lines, wide.lines)
|
||||||
|
|
||||||
|
def test_predecessor_spacing_is_part_of_the_key(self):
|
||||||
|
cache = {}
|
||||||
|
previous = self._document.add_paragraph()
|
||||||
|
previous.add_run("previous")
|
||||||
|
paragraph = self._document.add_paragraph()
|
||||||
|
paragraph.paragraph_format.space_before = Pt(12)
|
||||||
|
paragraph.add_run(_BODY)
|
||||||
|
alone = measure_paragraph(cache, paragraph, None, self._max_width)
|
||||||
|
collapsed = measure_paragraph(cache, paragraph, previous, self._max_width)
|
||||||
|
self.assertNotEqual(int(alone.before), int(collapsed.before))
|
||||||
|
self.assertEqual(self._fresh(paragraph, previous, self._max_width), collapsed)
|
||||||
|
|
||||||
|
def test_result_is_a_private_copy(self):
|
||||||
|
"""Paragraph.render() zeroes .before on a new page; the cache must not keep that."""
|
||||||
|
cache = {}
|
||||||
|
paragraph = self._document.add_paragraph()
|
||||||
|
paragraph.paragraph_format.space_before = Pt(12)
|
||||||
|
paragraph.add_run(_BODY)
|
||||||
|
first = measure_paragraph(cache, paragraph, None, self._max_width)
|
||||||
|
self.assertGreater(int(first.before), 0)
|
||||||
|
first.before = 0
|
||||||
|
second = measure_paragraph(cache, paragraph, None, self._max_width)
|
||||||
|
self.assertGreater(int(second.before), 0)
|
||||||
|
|
||||||
|
def test_attaching_the_predecessor_still_hits(self):
|
||||||
|
"""A figure is measured detached, then again after the Renderer attaches it."""
|
||||||
|
cache = {}
|
||||||
|
previous = self._document.add_paragraph()
|
||||||
|
previous.add_run("previous")
|
||||||
|
detached = self._document.add_paragraph()
|
||||||
|
detached.add_run(_BODY)
|
||||||
|
measure_paragraph(cache, detached, previous, self._max_width)
|
||||||
|
size = len(cache)
|
||||||
|
self._document._body._element.append(previous._p)
|
||||||
|
measure_paragraph(cache, detached, previous, self._max_width)
|
||||||
|
self.assertEqual(size, len(cache))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from md2gost.dnd import first_markdown, normalize_drop_paths, parse_tkdnd_files
|
from md2gost.dnd import first_markdown, normalize_drop_paths, parse_tkdnd_files
|
||||||
from md2gost.pipeline import (
|
from md2gost.pipeline import (
|
||||||
ConvertRequest,
|
ConvertRequest,
|
||||||
@@ -80,9 +82,8 @@ def test_parser_gui_and_optional_file():
|
|||||||
assert req.filename == "report.md"
|
assert req.filename == "report.md"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(sys.platform != "win32", reason="win32-only drag&drop API")
|
||||||
def test_win32_drop_api_pointer_width():
|
def test_win32_drop_api_pointer_width():
|
||||||
if sys.platform != "win32":
|
|
||||||
return
|
|
||||||
import ctypes
|
import ctypes
|
||||||
from md2gost.dnd import _win32_drop_api
|
from md2gost.dnd import _win32_drop_api
|
||||||
api = _win32_drop_api()
|
api = _win32_drop_api()
|
||||||
@@ -91,9 +92,8 @@ def test_win32_drop_api_pointer_width():
|
|||||||
api.CallWindowProc.argtypes[0](huge) # must not OverflowError
|
api.CallWindowProc.argtypes[0](huge) # must not OverflowError
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(sys.platform != "win32", reason="win32-only drag&drop API")
|
||||||
def test_win32_drop_hook_survives_window_messages():
|
def test_win32_drop_hook_survives_window_messages():
|
||||||
if sys.platform != "win32":
|
|
||||||
return
|
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from md2gost.dnd import enable_file_drop
|
from md2gost.dnd import enable_file_drop
|
||||||
root = tk.Tk()
|
root = tk.Tk()
|
||||||
|
|||||||
Binary file not shown.
@@ -1,13 +0,0 @@
|
|||||||
# *ВВЕДЕНИЕ
|
|
||||||
|
|
||||||
Текст.
|
|
||||||
|
|
||||||
%code1 Пример
|
|
||||||
|
|
||||||
```bash
|
|
||||||
line1
|
|
||||||
line2
|
|
||||||
line3
|
|
||||||
line4
|
|
||||||
line5
|
|
||||||
```
|
|
||||||
Binary file not shown.
Binary file not shown.
@@ -1,6 +1,10 @@
|
|||||||
version = 1
|
version = 1
|
||||||
revision = 3
|
revision = 3
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
|
resolution-markers = [
|
||||||
|
"python_full_version >= '3.12'",
|
||||||
|
"python_full_version < '3.12'",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "babel"
|
name = "babel"
|
||||||
@@ -176,6 +180,60 @@ 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" },
|
{ 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 = "contourpy"
|
||||||
|
version = "1.3.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
|
||||||
|
{ name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cycler"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "data-flow-diagram"
|
name = "data-flow-diagram"
|
||||||
version = "1.17.0"
|
version = "1.17.0"
|
||||||
@@ -200,6 +258,43 @@ 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" },
|
{ 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 = "fonttools"
|
||||||
|
version = "4.64.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d4/41/0f072a712dc74496e03710e462a18a4cfd8a258ad055a4e22d28b43a7abd/fonttools-4.64.0.tar.gz", hash = "sha256:ecb2e59a7bc692fee64dda6010deb66222335693b30046f15cccf81233aa715f", size = 3664266, upload-time = "2026-08-31T15:44:33.685Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/f7/a222d1e20d460a09d08fe0b612a6f373235168c6c3228ff6a913cc8ff9ea/fonttools-4.64.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:dac25768be4c03a990c359f408cb7e8958ed0e93061e495b3642ce7909761205", size = 3087538, upload-time = "2026-08-31T15:42:26.68Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/fd/571331ac2b9ea43403ba43f27f5b45427a13b5c559d4379c99f3f9437b59/fonttools-4.64.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d652592c71683941b768306fa1c7c6ce1bb9b072505043feafe86305d71030b7", size = 2582847, upload-time = "2026-08-31T15:42:28.664Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/e1/6f8a1a01e5ed4408fdffb934c77ce65d978a2df6fd6eaaec5e6b864b10f5/fonttools-4.64.0-cp311-cp311-win32.whl", hash = "sha256:c3c1fb656063a2f762db5378ea8d38ad5f7836b4f3fb8c4652270ded43df2935", size = 2439647, upload-time = "2026-08-31T15:42:40.196Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/21/41/b575f14a653911f33f17ef62bbeaa818c94bc7e694579cc9ddec6935d5b2/fonttools-4.64.0-cp311-cp311-win_amd64.whl", hash = "sha256:e63b63b8b5fdb8e29318dff2b15c5f852be46e972775b466f75b848f6eed4502", size = 2497436, upload-time = "2026-08-31T15:42:42.36Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/23/4ea251977fef70ed14193785e1b2949355f1f5927dc0ef1dada675c0bbb2/fonttools-4.64.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9ecb2b206b5b2386f6968721a0770226b66bdd54adc4279bfff3ddf62873eed8", size = 3095501, upload-time = "2026-08-31T15:42:44.299Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/32/943e9034f49797e1a25dbbd60c8047ce0c38c3585c5d1b2db38ce64059c5/fonttools-4.64.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8c631303bb1fd7be3067c47536a30ff1fcb4846d6008c112bc52a03f7cd6965", size = 2583091, upload-time = "2026-08-31T15:42:46.57Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/40/1251fef04c308836a3a7db523703a7ca8ab010dc1e3b9a9bf08e0861c7a8/fonttools-4.64.0-cp312-cp312-win32.whl", hash = "sha256:da4c9bdeaf6b06c12d13d0addfc8ef15aa9695d26574a6dc10751258bef72f30", size = 2430689, upload-time = "2026-08-31T15:42:58.48Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/1c/ce0e89183c0235ff6cfbf0603d593023a5afa5333efb35ff569cc0be9ce2/fonttools-4.64.0-cp312-cp312-win_amd64.whl", hash = "sha256:06b6409b868494556a831ae33b2d9a090476c37516b38d70f45a9720b460d423", size = 2482071, upload-time = "2026-08-31T15:43:00.598Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/7b/49b0054a79b9ed918c018e70e09b56eb5678ee8b44e59e126c79de4d8d73/fonttools-4.64.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9443eefff58aad558608f352092e1be6d278980e8c3b4e8621fcbfda97818500", size = 3092750, upload-time = "2026-08-31T15:43:02.705Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/83/e2/08e73bd2f6e6248f071d3e9debe4c2b4cecda3a7dc057c56b44e255421c3/fonttools-4.64.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:09657817b75575822bcd6098ef0ebf0386f34430839ee53109e70fd40a7f6539", size = 2583001, upload-time = "2026-08-31T15:43:04.923Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ff/25/7fee1978fdedc1a2d978b76dddbea88785416f5774e7222327b867712d71/fonttools-4.64.0-cp313-cp313-win32.whl", hash = "sha256:45e3ecc3888f1637094fd75cd8fc727f3a4b06d1ddf89181126c071e244fd2a5", size = 2429011, upload-time = "2026-08-31T15:43:16.463Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/ed/bb1c217c9e1fbfa59f42b266af57df937a703882a73e59a309773752228f/fonttools-4.64.0-cp313-cp313-win_amd64.whl", hash = "sha256:e4812f71c39d77ec5041348dafa400532adf7bf8f1fffa9aa6495fce5876d7b8", size = 2480199, upload-time = "2026-08-31T15:43:18.774Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/0a/59b9074b8ab165cc28d3e08bcf7a8eff8e1dc5932a5a41c87b15952fa3af/fonttools-4.64.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6f1ce9ef9a1b13098efdc2e43a2ed96d9851bbde7b31c652a87552c4efe9b422", size = 3096790, upload-time = "2026-08-31T15:43:21.125Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/17/39/af1077a36feefe79f36d67d75aee637a96673c0a74eeb2c6f24266f5a20b/fonttools-4.64.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:83cc48d1411d2ff388dab99973dca81172cc9ceae9c9799da9548d494cfb38cb", size = 2584288, upload-time = "2026-08-31T15:43:23.148Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3b/b0/15951b7e3006073f3260da4f8983732d18903905cfe26daa5138a0bbf682/fonttools-4.64.0-cp314-cp314-win32.whl", hash = "sha256:1c3661324f3f0fa4539a32288a3e0711a5f3ccf020036e760bb558ae9811a16f", size = 2432778, upload-time = "2026-08-31T15:43:34.421Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/f3/0c97402b29411f4f6d31b32f2af5b4bcd5babe5a93c64558a8fd3654f140/fonttools-4.64.0-cp314-cp314-win_amd64.whl", hash = "sha256:043f6c572bf236f2a76e762c25f841daea11e8fc03e78088d7be66e0c5b4e4c0", size = 2485394, upload-time = "2026-08-31T15:43:36.506Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/00/da/82192c7bee5314f04129a068dddae11082a63295d4e3b5ce08657de96608/fonttools-4.64.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4691a122b8c1d0d82d6e7510ce59d5c42146518240274b53e912e255573924f7", size = 3170118, upload-time = "2026-08-31T15:43:38.579Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/2d/0b4f608d754f625fecc7d94b3b26af6f65f6ab4527b386bb0c6141cdd9ea/fonttools-4.64.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:3200180abc69639483cf54a17cca2e13c31ede5f665979ea0a9c829d093f372f", size = 2617333, upload-time = "2026-08-31T15:43:40.934Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9b/cc/bffec3dccb9e3f4f6097a2baedaef5a8dec2cb816522ee163e4bb7f54b44/fonttools-4.64.0-cp314-cp314t-win32.whl", hash = "sha256:d30c966bea2deffa19c738c81776f7182da5ccabd97e666bae4f3d6ba87341d9", size = 2466542, upload-time = "2026-08-31T15:43:52.088Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/96/49/e31f97dfc94e0648999f04bfccb37a97062d978f87df60654f496942c89f/fonttools-4.64.0-cp314-cp314t-win_amd64.whl", hash = "sha256:917fd520bb60809d83c14d43cfe48d5ad2516abaf2c073d65a431800dade2d29", size = 2516703, upload-time = "2026-08-31T15:43:53.954Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/ec/e39b9e56db7dcc859983caa24cf2a36209dd71f6cc8ddbbf00d7342d7988/fonttools-4.64.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:8dd18fdff0ac9759b8d67a714730abee07b2312e3656c20ba5affb0107094762", size = 3091150, upload-time = "2026-08-31T15:43:56.309Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/e1/3e6a409d6f99efb66c2f6d0a71986432ff9cc9aca0df9ff4a8bed1d850f6/fonttools-4.64.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:5af87d1a6d247d7467ee082ae977a5443b2c45f8cd4d59375b6daa38d523c2de", size = 2582833, upload-time = "2026-08-31T15:43:58.348Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/f1/b15b845f66b559a24a9c40859391bbd3048af3ac0fd04e45b57dca11c4c7/fonttools-4.64.0-cp315-cp315-win32.whl", hash = "sha256:be084d19a3ac0c8b2aba696680642d703118d3b1f18cf83f5b7dbaf0ffc62ab6", size = 2431682, upload-time = "2026-08-31T15:44:09.293Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c6/b2/909beff0d2e2e1adac896e36cff7f9d76fd154bcd57e303d8f09aee8f72b/fonttools-4.64.0-cp315-cp315-win_amd64.whl", hash = "sha256:de8acaa5f4160f537a3cf41b031171d51004b9f4aebfa6c194f18dffa9533d03", size = 2484393, upload-time = "2026-08-31T15:44:11.117Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/40/47/06a51becf651cc071daa88e18ecb9f45ace9e3a570d8191ed8b3cae353fa/fonttools-4.64.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:5b90ad6637237b636d15c9ae8b7c4a7a1c194f33def378677e468c13fd4542f8", size = 3162235, upload-time = "2026-08-31T15:44:13.182Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/96/8b3faf58fd7ec3bb11943126658ad15b3f385068b23248deffccf2327ac1/fonttools-4.64.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:fa75c7970bc6bca340cc6e20f20f069201bfcb50094c31a536fd99724d1d01ca", size = 2613507, upload-time = "2026-08-31T15:44:15.123Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/21/ff/2eab37d43f2e2ccc0993959d8f2605d9a68470082c592684ac98611ee70a/fonttools-4.64.0-cp315-cp315t-win32.whl", hash = "sha256:f8669ce37851b597d3435b91fefa51139e58d506ca449ca0e5bb68c63b8b6d2b", size = 2463614, upload-time = "2026-08-31T15:44:27.198Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/8d/8bdff3ea656592197c8ef5062774221885468ffa01d8b0dc782dae23a83a/fonttools-4.64.0-cp315-cp315t-win_amd64.whl", hash = "sha256:89356c0793b474af7e49ec90d39fb2363e2341516a90460e38231df5ebe8acd5", size = 2512422, upload-time = "2026-08-31T15:44:29.478Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/f8/7188153c4b265c899cd035de6a062677d51f67118a4ba640902bd9683e90/fonttools-4.64.0-py3-none-any.whl", hash = "sha256:4a05783ff54ce4c7a28f18e5772efdf63c219374bd9ffc55452182e1cef8be60", size = 1195327, upload-time = "2026-08-31T15:44:31.741Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "freetype-py"
|
name = "freetype-py"
|
||||||
version = "2.5.1"
|
version = "2.5.1"
|
||||||
@@ -309,6 +404,63 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "kiwisolver"
|
||||||
|
version = "1.5.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ba/07/bd78e6a8fae171ea041ef5bba3ed21a003522fa088834b069b1909981f30/kiwisolver-1.5.1.tar.gz", hash = "sha256:f1303ef2eec81262a4b708c3e858afe58d7c75ad91c1c05266eda7673369859a", size = 104395, upload-time = "2026-08-28T10:28:27.153Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/7b/2de6908edc668427c149af5f93112e931f87e1fa4cab80bac32c5844dccc/kiwisolver-1.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b3d78f7bb2b9d9a30345be1474b9aaa8685430b54afb51ba3639b5c6c11e9ed6", size = 123364, upload-time = "2026-08-28T10:25:04.359Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/24/e70914415c77c97be7e22c80a0740869cb7428768cc380fdcdf6703e7084/kiwisolver-1.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5025e36fb4fb275cef0a4e30dbb11cb4ae61d1c83deb90189cb5d7e4cafd6b55", size = 66558, upload-time = "2026-08-28T10:25:05.506Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/2b/8b08b11833db4d475b8ef1f36174f8d8a7abd31bedd7e794be78e8814b48/kiwisolver-1.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc1a26b8e53395a01c2c611e58602fa47461f136fba7cd5542e6db6d64be1839", size = 64071, upload-time = "2026-08-28T10:25:06.7Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/9f/6f9e489c188200e6fb3193935501894811e8c97577c8ffe9033589bf3521/kiwisolver-1.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:1a7587dc335f2c0f5bd577fd0540bd16c66006bdb60f759a1059f025e6c4f071", size = 70744, upload-time = "2026-08-28T10:25:25.061Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c6/6d/dfc430d1d43957061599adea3f08ea982bb6f4ab601a8c974bedcf2ba850/kiwisolver-1.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:e4e4523d6f336708d732516e6cfca7796cf3d96c9474eb5aecf6165f2f1fefc3", size = 68404, upload-time = "2026-08-28T10:25:26.186Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6b/9b/65b302742389c6f96f2956bef5decf26011309feb2fc5d79613af18adea4/kiwisolver-1.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:63fb7294b768f444eb4b068965f2662f28c2fd4161e23bd60fcf3ff27b74c046", size = 123876, upload-time = "2026-08-28T10:25:27.44Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/71/74/c21f339956f6f691b2ed7e31d5f3ae767304df6c460192739fc830853051/kiwisolver-1.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0ebdef3eae5336568147c39a55be6a2036ffde53faa9ca2d978989ae7c2da12c", size = 66487, upload-time = "2026-08-28T10:25:28.728Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/84/e5/bdb34e21523e01dceda064d63713f3bdec91388af24fba1eca7ea5e85864/kiwisolver-1.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1798e83840c3f627246104c4d8a9639c60fa068adf9ce92b61791781fa8a68c1", size = 64660, upload-time = "2026-08-28T10:25:30.071Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6e/7c/7b210498f9f92e1cd7855f260fa69ef056881087b199ee20c208f0e4189a/kiwisolver-1.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:06a6917674de9e0fe3f66f5430787f59a9f2ddb64af9b714eaec547e29ef5c19", size = 70748, upload-time = "2026-08-28T10:25:48.444Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/61/ef0daa157c8bb23672f7423e0d14c39db1dc6ef8ed47e6bc54c9c1bef3bf/kiwisolver-1.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:ad8b9671348d7c8716715652ae11f85ed0eb99e265a2df2ca490577d69860b2c", size = 68324, upload-time = "2026-08-28T10:25:49.81Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/08/c1/88018321d976f53c421e379c43bc6993e70ce0c8a3ec5edc4bfe102257f6/kiwisolver-1.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b6ae6a0328f0bc035741820fdeecdcd67bf4694eee03972e843663107122f450", size = 62272, upload-time = "2026-08-28T10:25:51.02Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/85/d2/712bc17ea4f1d216034928069d612defbc6c95a471c55a7203a39faecb1a/kiwisolver-1.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:886fc26012f0e8b5f69d1cfe6d711f6b11f194621539bf8e6bb1c25c5dc82724", size = 64481, upload-time = "2026-08-28T10:25:52.22Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1b/e6/6c5380d676f43b6d918033962ea5e72360ca69e5a404154bc496b598ffdb/kiwisolver-1.5.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:aefe930d113798330e9462f7874542977869c0613cba3262e2de3a8d5dee8f3a", size = 66260, upload-time = "2026-08-28T10:25:53.387Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/6a/7087f5822cc8bb272679641404b1966a42504dac9ee74e2b33840475a0aa/kiwisolver-1.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5ca5aebae78a0bc13c1943af4af615d4966c5b650b05d5aa83b50e427196fee", size = 123876, upload-time = "2026-08-28T10:25:54.644Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/4b/9f385087ca09ee5ab9c09c6832561a7d2f7c78d3e5661d511e669f70e439/kiwisolver-1.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1ed0f5e49d0ceff8b72190824d9e59c062fbbc02c231b853112c78474b3f5ec2", size = 66487, upload-time = "2026-08-28T10:25:55.899Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/8b/40d33ffd2f378094ed462e9a9a0907e59d4de9845e65a59561272da350d4/kiwisolver-1.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:77a4c8187a5948d7f8795adb765a3c7b553d07d86d88e43038fc32fc1fb9a3f3", size = 64673, upload-time = "2026-08-28T10:25:57.048Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/22/deabbb3ad6d918d74b7831b2d8ae7151b09d21c87974e5ee8a456f58c94c/kiwisolver-1.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:dc23390afe9f4ef9ac3bcc72a03a56eebbde03f4c571a32cb38f859cff9a6524", size = 70758, upload-time = "2026-08-28T10:26:16.504Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b9/71/02fa5c2fd92068bb8952847e70ee6c5cb280e7febe11653d17812acc53dd/kiwisolver-1.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:186884a58486651e3c217b6acea0a53eaa9498fdd472057c46f2f0fb5c25aad5", size = 68329, upload-time = "2026-08-28T10:26:17.658Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/87/2d5dfad0daf17dcc18d98c48ed2332fc3f051cf599e60be6182a30dd4cf1/kiwisolver-1.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0324cd2567259b7a095f6cf18a52b0ffc6f3de9e69528ff1bc0e7a37bd43ff1a", size = 62337, upload-time = "2026-08-28T10:26:18.778Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/08/c8/83e1624f15d6262b470dbcc80b09979fd4d5b2ea3ddfc6b6e3327e235726/kiwisolver-1.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:74ea337e0ec3f6f342a36a4f1b5cd94dd9affddcd28ba9aae2905af932ee8c6b", size = 64513, upload-time = "2026-08-28T10:26:19.909Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c9/2d/827ec30eb07f528c08d8459ffb318ae91a56d793ee8acbea8b491f0ff906/kiwisolver-1.5.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ee9df1f0d77b9c6e94f4ac0fec533fbddd5ea3a327807f18d7b069ae019ded80", size = 66287, upload-time = "2026-08-28T10:26:21.078Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/53/11/5c43a562529dad8def4b81e5e1877c612a7e0298105a5939b3b409d2079c/kiwisolver-1.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fc271a6f0a2126958f4090e5507b9da5848927dae331f8f763bd4aa642b3d2cd", size = 123940, upload-time = "2026-08-28T10:26:22.475Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/db/9bd6c505c95128c258a55236bfbb3a7a3fb6023f863316b6d7d9f3c69052/kiwisolver-1.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9b3092d8992a1d69b7a59c3e39f35e1b9be327a17f68a7c35fc17329e337d6f2", size = 66493, upload-time = "2026-08-28T10:26:23.743Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e1/f4/b3007a3ed5c9be73f81161140684cf7d9bdb9c4b632f5f484d2a1c713fb9/kiwisolver-1.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c2306e8bb53601979fcb3fa09cc65e031876d9ae01eff2fcbcd7a84ef94d5bc1", size = 64720, upload-time = "2026-08-28T10:26:24.95Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4e/0e/49522e1ab5788cbaf63a26fbd3b851f9028616828c961b8a31b35cb96df8/kiwisolver-1.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:f4167e87b397f273dc2356fcf1eaf50a6bac51e6105f45103ef7129c8efb0255", size = 72282, upload-time = "2026-08-28T10:26:44.268Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/b6/22e7ca5315d363e6f81c9f37c9472e12e7b298731e77c0428e6a911a2c39/kiwisolver-1.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:5c490db2168a508088f59140dd392556a54b8bd1048fc6383c8baff13c359673", size = 69855, upload-time = "2026-08-28T10:26:45.725Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/8c/03a9cfbe871964c8758a816eb03ac96c806da2795a9a7cd9bf9648bfb594/kiwisolver-1.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4d4ca09bf13cff792b1884f64b98ee6c2467930d632233be25c56b442d99f10e", size = 126289, upload-time = "2026-08-28T10:26:47.023Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/e3/14ce3041ca79dff9c9d884ca00c7bf32374e76028a865a9ecd99b4f5a517/kiwisolver-1.5.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:44b8faef94f1857e77fa0238f3390ff1ac51d2ea20a487e2e452a59fd2b5f5ca", size = 67709, upload-time = "2026-08-28T10:26:48.268Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/af/c4/45030471a66ec8ef042e9f96ffe1d522c9ab12da180186a0898966fc1385/kiwisolver-1.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2ae70bc59790d2af72a3f76f24b272403e135070340281108b447cb77ea70819", size = 65909, upload-time = "2026-08-28T10:26:49.523Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/d4/14b21e4eb203c4d15425e8b6a2c625a320b4a1f2f7557eead63ffc30ffb7/kiwisolver-1.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5978c3340f16a35c30f8ab2fa7bcf559973c55f1a5ef6970e1f621acf3c4db13", size = 75404, upload-time = "2026-08-28T10:27:08.892Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/f5/53157899fc7f45f76421b77b99eb1639dd0f83f26ff9d76300c96bb4a3b0/kiwisolver-1.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ca307d6c259e5c98d3cb9ade55342b47a6839762caf2536f3d7b46ee660cc82e", size = 72946, upload-time = "2026-08-28T10:27:10.944Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/62/f786c3a27f181fa339d851a77e266d208e776b9883cabc40a5b041a31b5a/kiwisolver-1.5.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:bb7c99f0673c03017a3ee01e54a5c2617a05468b11eabe513b0080e063ed95b1", size = 62420, upload-time = "2026-08-28T10:27:12.308Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/cd/58a91ed25fbad0facdf503297b03768efab04bdf3141e5e3b49a34be7443/kiwisolver-1.5.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:0d8924877ce22e17326a99a418c3c82037da078df3c6a260b13eca677444e6e7", size = 64577, upload-time = "2026-08-28T10:27:13.502Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f6/5c/d501ef5a0958b226eac28306d24d5e5f114be0ace50e19cabae7b6b3b197/kiwisolver-1.5.1-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:534f02c1abb31ed6dbd3515545285c330b2f12d00fdb1fdb71658b9ca5a13a6a", size = 66284, upload-time = "2026-08-28T10:27:14.813Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/a6/8fbecaf4fc18c02f31f05e47a84c010a80e3ec391ed2f0bdade1d62b5954/kiwisolver-1.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:cea20da04494e662b83c872683bf4ff2345206043d036315ed0e924b652e7294", size = 124031, upload-time = "2026-08-28T10:27:16.193Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7c/c6/bf3090d2983b4204347cbdbe952116e7c3b2abf62b4e33e50167a13e75ee/kiwisolver-1.5.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:7fd82debf43c6acd0a94359d232f6bb516ee13f269a7993736a9ac9f988bb5d9", size = 66489, upload-time = "2026-08-28T10:27:17.506Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/85/de/562dddef55fdd7c291da8626d6619e72b5fc0870e6ccca0e149a5731e7f3/kiwisolver-1.5.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:18170a77ddfecf40ec60d0928268dc95880c881864e015a8f34094ed18b9b9ad", size = 64806, upload-time = "2026-08-28T10:27:18.673Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/0a/69a355e27f32ba50d5b6369949b6a1702e122f5277c89bc76d452b81c1c4/kiwisolver-1.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:958254518717542d02d0688d0d20cbf771da5e415e6f49543f92481c850a4540", size = 72283, upload-time = "2026-08-28T10:27:40.491Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/d8/7a95be90c33dcdd52204d4aa6384d731443225b887283bbd8b61e7931f6c/kiwisolver-1.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:da3275833be0edbaf4830fae08bae3dc7219f40ce0c37eaa6c25825957e06612", size = 69860, upload-time = "2026-08-28T10:27:41.835Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/f8/9bc493e7f5707788ba7f621902c68f82dc3a7ba03c78fbd337b026cef1ed/kiwisolver-1.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:470d420f98d368d6f010633a20659b544c5fdfa5329e6b70219f2ef08fd4a7ef", size = 126336, upload-time = "2026-08-28T10:27:43.379Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/82/3aea86b3f99712db825e9ac5631bf99571e818a8b8961ff98cebd798413e/kiwisolver-1.5.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:83f78128fa28705fa85d01c59771c72fe81c11bd0e6155edbb9f818983a7d761", size = 67698, upload-time = "2026-08-28T10:27:44.613Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/84/7d/8daafc5d2e7f9c47a4f78f8865d86d2a9cf399c2a85f86c44a993594410c/kiwisolver-1.5.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:9506e892bcc3b409831d363c6f53e5985e1c8d1f6f6b0256d00358684ff85378", size = 65945, upload-time = "2026-08-28T10:27:45.932Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/2d/8982c1fb7926da5bb7ed60318c3665b5c3f941447271ac982960a11b8637/kiwisolver-1.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:cb6fae641357ed2f6e533c0d3c6504a4a5703621a50c89459e46051d56b61140", size = 75379, upload-time = "2026-08-28T10:28:06.307Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/78/ba7b6dfa1708b82b373ac056928a30c545d5c1a627df9839dcec3c6c1881/kiwisolver-1.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:b390aec180a7c054919c04898835e1c77bced23ea8383eb2c570213bf25d1a86", size = 73011, upload-time = "2026-08-28T10:28:07.578Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a9/c4/1407df7512a5b36cc79840e01710dc575733c461b13ab866cae77eaf87f3/kiwisolver-1.5.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:482676e5bd48d70ac99d9fc78863469845421e01184fa83f1f9366dc49f7e974", size = 134002, upload-time = "2026-08-28T10:28:08.881Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a6/c1/69f00d627949580e43d57af0aa465df46868d7c29801c137a55374101294/kiwisolver-1.5.1-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:a5a00665d1a0e26763a7338d7e911d4598fbc1d50dd0d6b7919b7dc6c5d6569f", size = 73362, upload-time = "2026-08-28T10:28:12.449Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/1d/59ba570b1774e95e97fde3a0981b2e22118a7a495f73bf74cedc538566a0/kiwisolver-1.5.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:416ba7ff9f233b7036689bb5a3783537e838ad483f63558d2a800f75afe738b1", size = 59450, upload-time = "2026-08-28T10:28:20.383Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/98/a6849f04dc18b5400e8b98affa2cd8fd86ed583085f036e57b32e571f4fa/kiwisolver-1.5.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8af9b142ad719ae3a911ebf616bc4b78b32bbab84d6a40d3ad2f129670509957", size = 57400, upload-time = "2026-08-28T10:28:21.632Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/52/5aef56f21a460a6e43ab3cdfc7697d59d7b87deb0ec97a0f7b91aa4a521b/kiwisolver-1.5.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:17851e5dad4484be0cbccbde3b15331deae036de9aebd45eed964487802b172f", size = 98465, upload-time = "2026-08-28T10:28:25.696Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "latex2mathml"
|
name = "latex2mathml"
|
||||||
version = "3.81.0"
|
version = "3.81.0"
|
||||||
@@ -386,6 +538,52 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/d3/78/751d49c8bdfa0b30147c99235516433af6b63eca367ff28593c7346a0494/marko-2.2.4-py3-none-any.whl", hash = "sha256:d80510506edba096ec49d4720a09645fa0bb78e7b7b88697f20032fc19730aa9", size = 46753, upload-time = "2026-08-12T03:20:10.811Z" },
|
{ url = "https://files.pythonhosted.org/packages/d3/78/751d49c8bdfa0b30147c99235516433af6b63eca367ff28593c7346a0494/marko-2.2.4-py3-none-any.whl", hash = "sha256:d80510506edba096ec49d4720a09645fa0bb78e7b7b88697f20032fc19730aa9", size = 46753, upload-time = "2026-08-12T03:20:10.811Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matplotlib"
|
||||||
|
version = "3.11.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "contourpy" },
|
||||||
|
{ name = "cycler" },
|
||||||
|
{ name = "fonttools" },
|
||||||
|
{ name = "kiwisolver" },
|
||||||
|
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
|
||||||
|
{ name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pillow" },
|
||||||
|
{ name = "pyparsing" },
|
||||||
|
{ name = "python-dateutil" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/49/64/f9a391af28f518b11ad45a8a712353c94a0aefce09d3703200e5c54b610a/matplotlib-3.11.1.tar.gz", hash = "sha256:69647db5746941c793d6e445a4cd349323ffb87d9cc958c2ad84a659b4832d30", size = 32612045, upload-time = "2026-07-18T03:39:46.63Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6e/d0/791aa183dd88491555cf7d4be0b52b0bcf6c3c2a2c22c815a2e819bf53e2/matplotlib-3.11.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b7cf158e7add54a8d51ac9b5a84abd6d4e13ed4951b4f25f1c5139f41c2addb2", size = 9440302, upload-time = "2026-07-18T03:38:03.844Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/35/74/82bbdf683a301f4478384c8aaba6903631a2ca18294b2d7655c9a542bffb/matplotlib-3.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d2ace7273b9a5061a3b420918a16fae1f2dc5dfee1abcc13aba71b5d94b1820c", size = 9268549, upload-time = "2026-07-18T03:38:06.144Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/be/fa26ed085b41298f64a8f9b7592c671bbf1acc8b0df124c1c5de96b859f8/matplotlib-3.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:7f33a781e12b1e53b278deb2f5373c2e55ec4f10727be3440c0cfb5cda9f944f", size = 9315331, upload-time = "2026-07-18T03:38:14.949Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b6/f3/eb5bdf3b6e191b200db298b08bbc1638b7f3c82cdc8680f9d88bf72559ae/matplotlib-3.11.1-cp311-cp311-win_arm64.whl", hash = "sha256:67e4c3cd578c65ebd81bdc09a1b6592ceafee6dfafe116dc85dfcb647b5bbb18", size = 9003475, upload-time = "2026-07-18T03:38:17.205Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/6c/7ef7ebcb2bd9739b2b66b18b076e077f44bb46fdbe28ca0506edb3c62c79/matplotlib-3.11.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e15ef41507f3d525f46154ac9e3ae785dacde9f20e593a25de8986267892ef74", size = 9453849, upload-time = "2026-07-18T03:38:19.593Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/f8/6d0c312c8d9738e7d9677f09fe5c986b3239e651a7b73a2deb38b65e4a71/matplotlib-3.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:21a67b961a6d597bca54fae826cd20695ba4a6e4d05424a08da6e13e3176fd6b", size = 9283113, upload-time = "2026-07-18T03:38:21.95Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/89/56649bbaa2fd12e20f3be03dbcc135b0c8676d88bac17977599e3eb442a0/matplotlib-3.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:88a2a27dd9691ae448dfae4b26f59036be90c3c28757edd3553a29559d00859f", size = 9333886, upload-time = "2026-07-18T03:38:30.477Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/11/4d124efbbad677b7b7552f6f85a3bd432d4232f95400cea98fcd2ae36ef3/matplotlib-3.11.1-cp312-cp312-win_arm64.whl", hash = "sha256:480194afceca4df2f137c2721227d3cba67121fbf4397b69cee7f83714b0a58a", size = 9007545, upload-time = "2026-07-18T03:38:32.833Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/6c/4798363b7fb5644e309fe1fac30216e9146c9f70859d80d588c18caf5317/matplotlib-3.11.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6771b0cd7838c6a857a7209814158c0ad09bfef878db3033dd82d70ad101f191", size = 9454341, upload-time = "2026-07-18T03:38:35.001Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/98/6acadbe7f98df19d274bc107ac58bb439fa75df82c33dc110d71a4a8501f/matplotlib-3.11.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2abdee5ffa2fe11b2d19f7a5c63b785fb7c28cc46c7bc1814156341d9d1a33e1", size = 9283627, upload-time = "2026-07-18T03:38:37.061Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/3a/3d5e1f42dc761bf53401a62a83ff93389b37de9d2c093b2a3aa49ac34f1b/matplotlib-3.11.1-cp313-cp313-win_amd64.whl", hash = "sha256:68408341f2312836fbbdf6b3c78047f65b2d8752f5fd221c3e72d348f5b34f8b", size = 9334074, upload-time = "2026-07-18T03:38:46.616Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e2/db/3f5ea5a5b64060ef5e1ff60a19170423e41ce21b8497a6fe15a36e0b43e3/matplotlib-3.11.1-cp313-cp313-win_arm64.whl", hash = "sha256:0c1f44890d435c1b4ef52f701ad5828cb450ea97bcc83918fda6be74965d6cd2", size = 9007662, upload-time = "2026-07-18T03:38:49.112Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/6e/c7ae5e0531425b69c0826b00ebbc264c85cab853f1cd6e096c9983c2cdc1/matplotlib-3.11.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:5e510088c27a89d53580a752f959146893563e63c330e161d159b0fee652af6f", size = 9503790, upload-time = "2026-07-18T03:38:51.527Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/79/15be162e0a2ed546939674e2e97d0e33ec2447d86d4d4e611fa295bb178c/matplotlib-3.11.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:1524e2bdd48a93557aa47ddcfe9c225dfdd57d5a01a5c49128c20f0632980ee1", size = 9336148, upload-time = "2026-07-18T03:38:53.564Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/0f/a49c329d394f2e9ef38506982107e8b04ecf94dd41a9d8423ff82cc737c7/matplotlib-3.11.1-cp313-cp313t-win_amd64.whl", hash = "sha256:9a076f4fc5cdc43fdf510f5981418d25c2db4973418d9f22d8bb3dc8045ada78", size = 9383532, upload-time = "2026-07-18T03:39:02.468Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/50/103e86afb806d8f64d04ede14e4cfc09dbfc25f512421ff85fdd6ebd59cf/matplotlib-3.11.1-cp313-cp313t-win_arm64.whl", hash = "sha256:216fbb93a74add02ddb4cb38ef5348f59ac00b3e84567eaf16598772d40e150a", size = 9059665, upload-time = "2026-07-18T03:39:04.607Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/35/04/3079499fa8cb661ea66d13d6439d5a3ae6710a7afd5c7f72e08914f275f8/matplotlib-3.11.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:30c492d4ba9448595b6fd8708c6725963f8148e25c0d8842948da5b05f0ee8d3", size = 9456022, upload-time = "2026-07-18T03:39:07.041Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/53/a2/69acfe84ec1f32930e801a5782a07fc5c79c8c6599a507b806d859d5da8e/matplotlib-3.11.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ac104be2768ffdd8655db9e71b768cbb45f2b9aa7b450cf1595e8f65d3822319", size = 9285475, upload-time = "2026-07-18T03:39:09.562Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/04/d719a0a36930ecc8dfc801ff340f9dcfc4223f8ca5d39d06b4020032fff8/matplotlib-3.11.1-cp314-cp314-win_amd64.whl", hash = "sha256:9601a1e90be21e4884c53b4f3dc3ee0544654946f9975258d691f1c2e2f119c6", size = 9489571, upload-time = "2026-07-18T03:39:19.449Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/65/facabdc2f1f6caba7e856db64dfedddca25f7608df07d96a1c8fd114fd3b/matplotlib-3.11.1-cp314-cp314-win_arm64.whl", hash = "sha256:ae30c6109848ac0f9fa36c5d6270938487614c47ba31860bd5361266dabc5685", size = 9164486, upload-time = "2026-07-18T03:39:21.424Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/dd/18da6cd01cf96354534f98c468a25380c68ce582a2c9dd0cae12b04af4f2/matplotlib-3.11.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:dadfe80797174e2984aae3be0b77594a3c72d2c0a40fbd4a0de48d2728caf3ae", size = 9504876, upload-time = "2026-07-18T03:39:23.633Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/b0/f0b63555a18b79d038c81fd6126f35fc4dfce0eaff48d96103348c7cf935/matplotlib-3.11.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:89b193b255f4f6f7948dbcee3691f4f341ab05d9a8874a67b45ddb4182922eda", size = 9336120, upload-time = "2026-07-18T03:39:25.797Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f6/4a/907ed190ee81a9df581e0ed5456134fc0f7cb55ffcfda2f9e54ca900761c/matplotlib-3.11.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f2912f647f3fbe1ccf085f91e213936f9101bead81a5e670565b1f1b3712f4fb", size = 9540074, upload-time = "2026-07-18T03:39:34.789Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/d4/97c19b77e0a6e3b48581185bb65088f431cd20186076cc0f650a1757ea46/matplotlib-3.11.1-cp314-cp314t-win_arm64.whl", hash = "sha256:54d47b8ae8b579633a3902ca5b4ad6c1e132a5626d64447b2e22a66394e79987", size = 9213472, upload-time = "2026-07-18T03:39:37.141Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ee/38/ceb1d637c4db6d06141f3739e93af3321e7caaabe69b57ae48ffe3ee95b1/matplotlib-3.11.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:427258425f9a3fc4ed79a91f9e9b9aaf5a82cb6571e85dc14063cc6fbb993741", size = 9438045, upload-time = "2026-07-18T03:39:39.491Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/25/72ad8b58602d3a6ef1dfc4b65ecd01634ab65a2bdf494c9fe0e966dbf081/matplotlib-3.11.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:1ac697e591c11b6ad04679a73c2d2f9980fe9d9f0311fb414a2e329706343dfb", size = 9266127, upload-time = "2026-07-18T03:39:41.597Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "md2gost"
|
name = "md2gost"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -397,11 +595,13 @@ dependencies = [
|
|||||||
{ name = "latex2mathml" },
|
{ name = "latex2mathml" },
|
||||||
{ name = "lxml" },
|
{ name = "lxml" },
|
||||||
{ name = "marko" },
|
{ name = "marko" },
|
||||||
|
{ name = "matplotlib", marker = "sys_platform != 'linux'" },
|
||||||
{ name = "mermaidx" },
|
{ name = "mermaidx" },
|
||||||
{ name = "pillow" },
|
{ name = "pillow" },
|
||||||
{ name = "playwright" },
|
{ name = "playwright" },
|
||||||
{ name = "pygments" },
|
{ name = "pygments" },
|
||||||
{ name = "python-docx" },
|
{ name = "python-docx" },
|
||||||
|
{ name = "pywin32", marker = "sys_platform == 'win32'" },
|
||||||
{ name = "requests" },
|
{ name = "requests" },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -423,11 +623,13 @@ requires-dist = [
|
|||||||
{ name = "latex2mathml", specifier = ">=3.76.0,<4" },
|
{ name = "latex2mathml", specifier = ">=3.76.0,<4" },
|
||||||
{ name = "lxml", specifier = ">=4.9,<6" },
|
{ name = "lxml", specifier = ">=4.9,<6" },
|
||||||
{ name = "marko", specifier = ">=2.0.0,<3" },
|
{ name = "marko", specifier = ">=2.0.0,<3" },
|
||||||
|
{ name = "matplotlib", marker = "sys_platform != 'linux'", specifier = ">=3.7" },
|
||||||
{ name = "mermaidx", specifier = ">=0.9.0,<1" },
|
{ name = "mermaidx", specifier = ">=0.9.0,<1" },
|
||||||
{ name = "pillow", specifier = ">=10.0.0,<11" },
|
{ name = "pillow", specifier = ">=10.0.0,<11" },
|
||||||
{ name = "playwright", specifier = ">=1.40.0,<2" },
|
{ name = "playwright", specifier = ">=1.40.0,<2" },
|
||||||
{ name = "pygments", specifier = ">=2.16.1,<3" },
|
{ name = "pygments", specifier = ">=2.16.1,<3" },
|
||||||
{ name = "python-docx", specifier = ">=0.8.11,<1" },
|
{ name = "python-docx", specifier = ">=0.8.11,<1" },
|
||||||
|
{ name = "pywin32", marker = "sys_platform == 'win32'" },
|
||||||
{ name = "pywin32", marker = "sys_platform == 'win32' and extra == 'word'", specifier = ">=306" },
|
{ name = "pywin32", marker = "sys_platform == 'win32' and extra == 'word'", specifier = ">=306" },
|
||||||
{ name = "requests", specifier = ">=2.31.0,<3" },
|
{ name = "requests", specifier = ">=2.31.0,<3" },
|
||||||
]
|
]
|
||||||
@@ -449,6 +651,114 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/7a/62/39480a6aa506aa756021802eedde019f70aa3a15525f377708f705aa5fc8/mermaidx-0.9.5-py3-none-any.whl", hash = "sha256:46dfbdc7c101bd5395f05bca2f0917c467b9c5d5c3cbec1fbe238c546b673039", size = 1776497, upload-time = "2026-08-31T02:44:09.368Z" },
|
{ url = "https://files.pythonhosted.org/packages/7a/62/39480a6aa506aa756021802eedde019f70aa3a15525f377708f705aa5fc8/mermaidx-0.9.5-py3-none-any.whl", hash = "sha256:46dfbdc7c101bd5395f05bca2f0917c467b9c5d5c3cbec1fbe238c546b673039", size = 1776497, upload-time = "2026-08-31T02:44:09.368Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "numpy"
|
||||||
|
version = "2.4.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
resolution-markers = [
|
||||||
|
"python_full_version < '3.12'",
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "numpy"
|
||||||
|
version = "2.5.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
resolution-markers = [
|
||||||
|
"python_full_version >= '3.12'",
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/13/01/11703282db468b85f6f7b8c7f22d058de5970d5c7e60a3a8aaa313c3de36/numpy-2.5.3.tar.gz", hash = "sha256:df2d5874ff183595a4ba404edd04f6bd9b5505c1d7708573f6a6c17489a67563", size = 20791231, upload-time = "2026-09-06T16:27:47.073Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/50/8fdbb16af64895706a45f06a4068e29db732ec180f3c1375f14123359138/numpy-2.5.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb189f09db39283b26bfd061ec16189e14f71c6755207f72a0f7540867afe5b9", size = 16994982, upload-time = "2026-09-06T16:24:29.244Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/39/789131c1188c078dcb3a1692e72e1e050c68b88ffe72c9ccaac9bcd7a9cd/numpy-2.5.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f59a878c33d6b88122d80d239bb3b845d58708750b0cb06a09aebb9b18ec696c", size = 12009327, upload-time = "2026-09-06T16:24:32.491Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/59/a312e95696e5f601914dd8b6dd844692ba61670807417e24b68e337b5c70/numpy-2.5.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a72f874bc9e10e4b8f80426fb49716d5141f64442a0c8418065093ec8017fbb0", size = 5445405, upload-time = "2026-09-06T16:24:35.071Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/d0/5623a1707ed4fe16e3909fe3cf5ee3da004ae677ad23d83bbf3adf1a6faf/numpy-2.5.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:fc36dc566135b5eceec4cf89758fcb719266a019ef07dae1754ae7c9f617ef3e", size = 6783213, upload-time = "2026-09-06T16:24:37.253Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/db/35e1c2d38b04cbd5b731f9d71495e055e813197669d22b612f11748d2ff9/numpy-2.5.3-cp312-cp312-win32.whl", hash = "sha256:bf63afbe037eb5d2fe87fbcc7778e61da53ebaf21d938a4515aa73b62532a5d4", size = 6133378, upload-time = "2026-09-06T16:24:51.915Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/a1/accf6d4f0c80c5d9ba9735d6b1550e444180599f34dec69ca01360f717ad/numpy-2.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:0a59a421a32580a009e8a1751345bf829631b990dc1794b80514ab722b435def", size = 12567828, upload-time = "2026-09-06T16:24:54.255Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/43/1764aff32e4652526ae2f71fa8b3efd8d25c8a3d6926914454e47138ed1e/numpy-2.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:ccb32e0525d29e8b0572eb84c9a57af0e7a4e615726927506f55063c62414034", size = 10485432, upload-time = "2026-09-06T16:24:57.278Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/e5/8fb89cd46d14e35699d13bf943a5f5f441ecee8667120a1f6105ab89e349/numpy-2.5.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:66a78fe4556c60aceda5916f9eacd638b18e9e681016ec302dcb4682d6d4d034", size = 16991061, upload-time = "2026-09-06T16:25:00.411Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2f/06/9dc9e48b5e5e941c8b10350c5ff2d721da42a20517d911d15544246775ff/numpy-2.5.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92f30e89b8ee0ecf363033576c422b2f58fed6a80bed0aa48dff6d14c654663e", size = 12003676, upload-time = "2026-09-06T16:25:03.475Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ab/2a/98282aa5b8f58b1157d440bb6282eed47e3632a5de53a714fbab17e659fe/numpy-2.5.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f9a2353b37a1a9e78fd82b27ad7e2a32a2d036604d18f02b05e3136c62ca3b09", size = 5439695, upload-time = "2026-09-06T16:25:05.978Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/f9/b6533d777be9d6ffd29dc1be0867e563e6e8cc9a220ff1b716adc317f060/numpy-2.5.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:ccbc4665079665c3cf3bab4db9f6b095370cd6437d66be549b6c2a1fd19e1958", size = 6779395, upload-time = "2026-09-06T16:25:08.599Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/41/ee/38e785e88a4045f6ad1d1f2808dcdfafdca48c760260c0587bf171e29fc9/numpy-2.5.3-cp313-cp313-win32.whl", hash = "sha256:1c80eabb4035ecf4ca9cd49cde8a9fdd69a729e63e6474887d1523ade7aa277f", size = 6129003, upload-time = "2026-09-06T16:25:23.664Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/ec/100f2b1794ede74a9b3d7ec6b9736927f56713414c1dfe19ab6c383494bf/numpy-2.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:71cad2b2a7451ab79d8f5e71b453485b6775963d5cf794179144a7463fe6e8ec", size = 12560965, upload-time = "2026-09-06T16:25:26.602Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/80/b1/7dc825ca94c12acebbce4c37caa5e198695eb31424bc579679f32b1bb49d/numpy-2.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:8e4dd766076855b5ff7ea52fa5f07ce26286726e0f8bff446b7739d02e6ea204", size = 10482343, upload-time = "2026-09-06T16:25:29.772Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/78/cf416f15dc29375a229d9dfebf8db6e313f291580b39fa1a568b6052bb07/numpy-2.5.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:350ba9783ce969cf9f7ce6e6a9a58e1a6e2a19ca025b7ee448c4db727706212a", size = 16998686, upload-time = "2026-09-06T16:25:33.171Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/59/abcc2d8def4fd60eec7d87f92d27c13448ffd9ab14339bcc63a0d7a2fdea/numpy-2.5.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:012e66aca395d795496446e52aeeb5866312a5d4d3f27da270e5a0b43f70dc5c", size = 12013862, upload-time = "2026-09-06T16:25:36.748Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/75/4640d2d6e4b64a049e48425a82728a41ef4adb61332d2cba68055774878b/numpy-2.5.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:adc1ada2662f8a5f960b8a10d9986897e7499ef07e06d4cfe7197f8cce923c07", size = 5449793, upload-time = "2026-09-06T16:25:39.476Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/96/cd/625b57ae33d4ca560f32cc0b47b4a5922146d9beb998ddf773900d440a73/numpy-2.5.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:54a115e5a73b8fc44f0cebef486365a1894b5c9760685d4558b72b7c3eb846e0", size = 6785176, upload-time = "2026-09-06T16:25:42.069Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/2c/dfa40f6991f8185c8c30ffd023dfcbb11888e823cfab9557b920f3bb7bed/numpy-2.5.3-cp314-cp314-win32.whl", hash = "sha256:c2381f82999704f818e2c987a865050e285ec3621262c66d40f5a96c8f899f8e", size = 6180485, upload-time = "2026-09-06T16:25:58.157Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/73/d2c08231e4fde7e415501fd02c715d96e98599b2d8384445933944152984/numpy-2.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:2c25dfa72943e4336ddb6b0ee4277b47a0c85bede0807530ec68103bf58e2c10", size = 12698179, upload-time = "2026-09-06T16:26:00.789Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/e9/dcdcc9b95cf5f49815055573aee1b11cfbf5299f38a180e437ded050810f/numpy-2.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:15aa985ac73a8db02db7663381aa109510449d3819d37206caed27b33a65a8a6", size = 10769383, upload-time = "2026-09-06T16:26:04.011Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/c4/af8bc08a7ef4e1529a7c0cf24969accce316b783999802089a581ec99272/numpy-2.5.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ac7bb1c52d445bd4f8f7f97fefe6abc3a084dc4d63df50d79b17fa2b78e89297", size = 12132668, upload-time = "2026-09-06T16:26:07.138Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c5/ae/0f15eb56d4ec5e13c1f7ff04ff407f997d1acbadb45d3e1f2e2645a8f43c/numpy-2.5.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e6ab667ba76450084eb64013762c438ea76d9d29cc676dcd6c2e9892ba37f841", size = 5568580, upload-time = "2026-09-06T16:26:09.828Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/fb/c72a8f25d4b6e96c354e7ab45ace3b27dc11e5d6a13b6c7d0cd6b08bf112/numpy-2.5.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:f7fabeb6cea87d65f3b926de33d03fb016cfdc29314c90974383b5582ae72891", size = 6882634, upload-time = "2026-09-06T16:26:12.524Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/39/3453afb7119d0449ef11c886874120ff180e2c337760e0e2d88f70f1a945/numpy-2.5.3-cp314-cp314t-win32.whl", hash = "sha256:4c8a6d2ebce6305fd82fbefca827775437147052a976ee7c94b36a0c1b52ac6c", size = 6335421, upload-time = "2026-09-06T16:26:28.175Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/01/22815d2b19a1a746b1d45205cffebb3fe511a18acb75fba6c88491fc9894/numpy-2.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:9a37475425b431b4d060f23b4f52cd2f3aef6bc7c654bd760adf0040eec9d435", size = 12896420, upload-time = "2026-09-06T16:26:31.265Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/ee/a7cbba67eeaff038dc29ca8b98a88396c8b0cc9c89d4924f4a27a5c9150b/numpy-2.5.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2d8240cb4c16fd831074aa2b2cf9fc54664d826341d61c372245b96a74a49a9a", size = 10857177, upload-time = "2026-09-06T16:26:34.167Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/56/78194492883ff5eec90423fe56a3a44b154da047d88a6307f629713c584f/numpy-2.5.3-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a6391fafaba97500887132cd582abc6e19452b1ac775a47caa7b24490e152058", size = 16996531, upload-time = "2026-09-06T16:26:37.287Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/11/39/dd55c0af90bbab564b09ae3b0aa60ec5c02b900fa4f1ba23440525c8b32d/numpy-2.5.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:09d5a423c71ad5feb5625844ad58050e35df43871004b52ac9c0ad44a56775be", size = 12012569, upload-time = "2026-09-06T16:26:40.707Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b6/51/04f67d32e4862b281b1cb84ceeaed3421189a84fb6fb51a391cd6d5009f7/numpy-2.5.3-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:f9579f383d1bf9df80081e72760e84960a7fd4f88cf0c9e535a8597c9bb646f5", size = 5448498, upload-time = "2026-09-06T16:26:43.435Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/c9/25b4dc0dd1344ec26c7319e84fd4e9809d2b5628f4e12decd618036e5178/numpy-2.5.3-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:86bff898a431c0fb71f7610b75726e75a54d47b37edc9d537f48de63bb3c0b90", size = 6783026, upload-time = "2026-09-06T16:26:46.374Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/01/bc/b5e90a91c115168d793dfd2ad9c69c438c2fe7a13a437e770bc5b078e732/numpy-2.5.3-cp315-cp315-win32.whl", hash = "sha256:e01c918ac3d48e18a927cf7b14a26a3e29ff2bdf2eacb976da0aecd6a43ed034", size = 6179919, upload-time = "2026-09-06T16:27:03.166Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/37/ea/780748fd3985109075514ef8fc64cd25f943e40dde13a6d59141eb268fc8/numpy-2.5.3-cp315-cp315-win_amd64.whl", hash = "sha256:e931e4f499e0dc7ef29d269a8e5b35dd722e5d14be07df6240166ea7c6532fae", size = 12697656, upload-time = "2026-09-06T16:27:06.153Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/16/407be69a2a87c8cab64d95975a8977a426a29e138f07e276ec258f0fe4e5/numpy-2.5.3-cp315-cp315-win_arm64.whl", hash = "sha256:26e15e4aecd8617dfbaecb37d223e365d7b39411fba20454be2670a96aa74cb5", size = 10767601, upload-time = "2026-09-06T16:27:09.297Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/bf/a97ffb01e41d50a32a9177aef942a4d0e389a3daf451d04e5f38ef6afb87/numpy-2.5.3-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:6cef4bb1706dfec49243c05d921eefb4e190d41e2528b30d8035ea1f36b4c24a", size = 17090092, upload-time = "2026-09-06T16:27:12.907Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/24/136c02f2c2af9a067a84d0c3aa10c99012c0476fa5066732fa4a4202557d/numpy-2.5.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:d1c89973648c85069c5046ad460f7b8a00218b29a2e42359ac8cc63e9ab94832", size = 12129429, upload-time = "2026-09-06T16:27:16.089Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/6c/b47582d6597789bf946d5efbeb6b9e56fd8bcbd5efc6fbf51dbe1ea31eb3/numpy-2.5.3-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:214045a5bf00113a146ab9ee9730c44501af6723cdf1f6830932f7b5ef2e7af0", size = 5565452, upload-time = "2026-09-06T16:27:19.868Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/b4/ef3cc6da73774202d4deae16bb321fd8298a4e0561e3539f8c4be237d916/numpy-2.5.3-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:8617bbfae4486cf99c9f899966699428d19da931d06ca94ad3da986c76e15997", size = 6876736, upload-time = "2026-09-06T16:27:22.232Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/d2/4e1014173aa3c55e6a756e0e567290743a6ab33a288460374d7ef6bcd239/numpy-2.5.3-cp315-cp315t-win32.whl", hash = "sha256:f54660b0eb6b0b9f36e7fe1cdfdff472028dd0d14acd9b9b65098efbad059469", size = 6330292, upload-time = "2026-09-06T16:27:38.149Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6c/b0/ff5658a58199b7bcaad87bf260eef6713d9d42cca4e028f935b4fc5fbac6/numpy-2.5.3-cp315-cp315t-win_amd64.whl", hash = "sha256:1aad64d99730d013cfc6debafed22783b4fc5a7f4b8bc744d2d8cf7dcc880551", size = 12884918, upload-time = "2026-09-06T16:27:40.965Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fb/0b/b12a2df5d1b774bd9007a6fdff9381145b6223d37f11afc9c37ab0efd9a1/numpy-2.5.3-cp315-cp315t-win_arm64.whl", hash = "sha256:befa1ae5bd6030b3f512b43ff3fa5290bbed6b84411a44244b14adf835f5b89d", size = 10850807, upload-time = "2026-09-06T16:27:43.868Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "packaging"
|
name = "packaging"
|
||||||
version = "26.3"
|
version = "26.3"
|
||||||
@@ -548,6 +858,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
|
{ url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyparsing"
|
||||||
|
version = "3.3.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest"
|
name = "pytest"
|
||||||
version = "9.1.1"
|
version = "9.1.1"
|
||||||
@@ -564,6 +883,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
|
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-dateutil"
|
||||||
|
version = "2.9.0.post0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "six" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-docx"
|
name = "python-docx"
|
||||||
version = "0.8.11"
|
version = "0.8.11"
|
||||||
|
|||||||
+214
-190
@@ -3,7 +3,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from typing import Callable
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
from docx import Document
|
from docx import Document
|
||||||
from docx.oxml.ns import qn
|
from docx.oxml.ns import qn
|
||||||
@@ -27,7 +28,6 @@ from .blocks import (
|
|||||||
)
|
)
|
||||||
from .captions import caption_unique_id, parse_any_caption
|
from .captions import caption_unique_id, parse_any_caption
|
||||||
from .classify import (
|
from .classify import (
|
||||||
caption_kind_from_style,
|
|
||||||
classify_heading_text,
|
classify_heading_text,
|
||||||
heading_level_from_style,
|
heading_level_from_style,
|
||||||
is_bibliography_style,
|
is_bibliography_style,
|
||||||
@@ -150,6 +150,16 @@ def _section_landscape(document: Document, element) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class _Para:
|
||||||
|
"""The few paragraph facts every `_handle_paragraph` branch needs."""
|
||||||
|
|
||||||
|
paragraph: DocxParagraph
|
||||||
|
element: Any
|
||||||
|
style: str
|
||||||
|
text: str
|
||||||
|
|
||||||
|
|
||||||
class DocxWalker:
|
class DocxWalker:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -222,210 +232,224 @@ class DocxWalker:
|
|||||||
|
|
||||||
def _handle_paragraph(self, paragraph: DocxParagraph) -> list[Block]:
|
def _handle_paragraph(self, paragraph: DocxParagraph) -> list[Block]:
|
||||||
out: list[Block] = []
|
out: list[Block] = []
|
||||||
style = style_name(paragraph)
|
para = _Para(
|
||||||
text = _para_plain(paragraph)
|
paragraph=paragraph,
|
||||||
el = paragraph._element
|
element=paragraph._element,
|
||||||
|
style=style_name(paragraph),
|
||||||
|
text=_para_plain(paragraph),
|
||||||
|
)
|
||||||
|
|
||||||
# Page break
|
self._emit_page_breaks(para, out)
|
||||||
if self.emit_page_breaks:
|
if not self._reached_body_start(para):
|
||||||
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
|
return out
|
||||||
|
|
||||||
# Native TOC field detection
|
for handler in (
|
||||||
if self._has_toc_field(el):
|
self._as_toc_style,
|
||||||
self._flush_code(out)
|
self._as_toc_field,
|
||||||
self._flush_list(out)
|
self._as_heading,
|
||||||
if not self.keep_toc_pages:
|
self._as_caption,
|
||||||
# only placeholder once after СОДЕРЖАНИЕ heading
|
self._as_code_line,
|
||||||
return out
|
self._as_images,
|
||||||
return out
|
self._as_bibliography,
|
||||||
|
self._as_numbered_list_item,
|
||||||
|
self._as_marker_list_item,
|
||||||
|
):
|
||||||
|
handled = handler(para, out)
|
||||||
|
if handled is not None:
|
||||||
|
return handled
|
||||||
|
|
||||||
level = heading_level_from_style(style)
|
return self._as_plain_paragraph(para, out)
|
||||||
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
|
def _emit_page_breaks(self, para: _Para, out: list[Block]) -> None:
|
||||||
if is_caption_style(style) or parse_any_caption(text):
|
if not self.emit_page_breaks:
|
||||||
parsed = parse_any_caption(text)
|
return
|
||||||
kind = caption_kind_from_style(style) if is_caption_style(style) else None
|
for br in para.element.findall(".//" + qn("w:br")):
|
||||||
if parsed is None and kind:
|
if br.get(qn("w:type")) == "page":
|
||||||
# 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_code(out)
|
||||||
self._flush_list(out)
|
self._flush_list(out)
|
||||||
if kind and parsed.kind != kind and kind != "figure":
|
out.append(PageBreakBlock())
|
||||||
# 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
|
def _reached_body_start(self, para: _Para) -> bool:
|
||||||
if is_code_style(style):
|
"""Front matter (title page, assignment) is dropped until the first Heading 1."""
|
||||||
self._flush_list(out)
|
if self._started:
|
||||||
if self._pending_caption and self._pending_caption.kind == "listing":
|
return True
|
||||||
self._code_caption = self._pending_caption
|
if is_body_start_heading(para.text, para.style) or heading_level_from_style(para.style) == 1:
|
||||||
self._pending_caption = None
|
self._started = True
|
||||||
self._code_buf.append(paragraph_raw(paragraph))
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _as_toc_style(self, para: _Para, out: list[Block]) -> list[Block] | None:
|
||||||
|
if not is_toc_style(para.style):
|
||||||
|
return None
|
||||||
|
if not self.keep_toc_pages:
|
||||||
return out
|
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_code(out)
|
||||||
self._flush_list(out)
|
self._flush_list(out)
|
||||||
if not text and not paragraph_has_omml(el):
|
out.append(ParagraphBlock(spans=_inline_spans(para.paragraph), style=para.style))
|
||||||
|
return out
|
||||||
|
|
||||||
|
def _as_toc_field(self, para: _Para, out: list[Block]) -> list[Block] | None:
|
||||||
|
if not self._has_toc_field(para.element):
|
||||||
|
return None
|
||||||
|
self._flush_code(out)
|
||||||
|
self._flush_list(out)
|
||||||
|
return out
|
||||||
|
|
||||||
|
def _as_heading(self, para: _Para, out: list[Block]) -> list[Block] | None:
|
||||||
|
level = heading_level_from_style(para.style)
|
||||||
|
is_heading = level is not None or (
|
||||||
|
para.text and is_special_title(para.text) and not para.text.startswith("[")
|
||||||
|
)
|
||||||
|
if not is_heading:
|
||||||
|
return None
|
||||||
|
|
||||||
|
self._flush_code(out)
|
||||||
|
self._flush_list(out)
|
||||||
|
if not para.text:
|
||||||
return out
|
return out
|
||||||
spans = _inline_spans(paragraph)
|
|
||||||
# If whole paragraph is only omml (block-ish), emit equation
|
lvl, title, numbered = classify_heading_text(para.text, level or 1)
|
||||||
oms = find_omml(el)
|
self._pending_caption = None
|
||||||
if oms and not text.replace(" ", ""):
|
if title.upper() == "СОДЕРЖАНИЕ" or title.upper().startswith("СОДЕРЖАНИЕ"):
|
||||||
|
out.append(HeadingBlock(level=1, text="СОДЕРЖАНИЕ", numbered=False))
|
||||||
|
out.append(TocPlaceholder())
|
||||||
|
self._saw_toc_heading = True
|
||||||
|
return out
|
||||||
|
out.append(HeadingBlock(level=lvl, text=title, numbered=numbered))
|
||||||
|
return out
|
||||||
|
|
||||||
|
def _as_caption(self, para: _Para, out: list[Block]) -> list[Block] | None:
|
||||||
|
if not (is_caption_style(para.style) or parse_any_caption(para.text)):
|
||||||
|
return None
|
||||||
|
parsed = parse_any_caption(para.text)
|
||||||
|
if parsed is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
self._flush_code(out)
|
||||||
|
self._flush_list(out)
|
||||||
|
cap = CaptionBlock(
|
||||||
|
kind=parsed.kind,
|
||||||
|
number=parsed.number,
|
||||||
|
text=parsed.text,
|
||||||
|
is_continuation=parsed.is_continuation,
|
||||||
|
unique_id=caption_unique_id(parsed.kind, parsed.number),
|
||||||
|
)
|
||||||
|
# Tables/listings: caption precedes the object → keep pending.
|
||||||
|
# Continuations are spliced by postprocess_blocks; figures (GOST) are
|
||||||
|
# captioned *under* the picture and folded later. Stashing either would
|
||||||
|
# glue the caption onto the next object.
|
||||||
|
self._pending_caption = (
|
||||||
|
cap if not parsed.is_continuation and parsed.kind in ("table", "listing") else None
|
||||||
|
)
|
||||||
|
out.append(cap)
|
||||||
|
return out
|
||||||
|
|
||||||
|
def _as_code_line(self, para: _Para, out: list[Block]) -> list[Block] | None:
|
||||||
|
if not is_code_style(para.style):
|
||||||
|
return None
|
||||||
|
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(para.paragraph))
|
||||||
|
return out
|
||||||
|
|
||||||
|
def _as_images(self, para: _Para, out: list[Block]) -> list[Block] | None:
|
||||||
|
rids = iter_blip_rids(para.element)
|
||||||
|
if not rids:
|
||||||
|
return None
|
||||||
|
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(para.paragraph.part, rid)
|
||||||
|
if not rel:
|
||||||
|
self.log(f"Не удалось извлечь картинку {rid}")
|
||||||
|
continue
|
||||||
|
out.append(
|
||||||
|
ImageBlock(
|
||||||
|
rel_path=rel,
|
||||||
|
alt=para.text or "",
|
||||||
|
caption_id=cid,
|
||||||
|
caption_text=text_c,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
cid = text_c = None # only first drawing gets pre-caption
|
||||||
|
return out
|
||||||
|
|
||||||
|
def _as_bibliography(self, para: _Para, out: list[Block]) -> list[Block] | None:
|
||||||
|
biblio = parse_biblio_line(para.text)
|
||||||
|
if not (is_bibliography_style(para.style) or biblio):
|
||||||
|
return None
|
||||||
|
self._flush_code(out)
|
||||||
|
self._flush_list(out)
|
||||||
|
if not biblio:
|
||||||
|
return None # bibliography style but no [n] marker — treat as plain text
|
||||||
|
key, body = biblio
|
||||||
|
out.append(
|
||||||
|
ParagraphBlock(
|
||||||
|
spans=[InlineSpan(text=f"[{key}]: {body}")],
|
||||||
|
style=para.style,
|
||||||
|
is_bibliography=True,
|
||||||
|
biblio_key=key,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return out
|
||||||
|
|
||||||
|
def _as_numbered_list_item(self, para: _Para, out: list[Block]) -> list[Block] | None:
|
||||||
|
if not paragraph_has_numpr(para.paragraph):
|
||||||
|
return None
|
||||||
|
self._flush_code(out)
|
||||||
|
# python-docx does not expose the rendered number, so infer from the text
|
||||||
|
m = re.match(r"^\d+\.\s*(.*)$", para.text)
|
||||||
|
if m:
|
||||||
|
ordered, body = True, m.group(1)
|
||||||
|
else:
|
||||||
|
ordered, body = False, re.sub(r"^[–—\-•▪]\s*", "", para.text)
|
||||||
|
self._append_list_item(ordered, body, out)
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _as_marker_list_item(self, para: _Para, out: list[Block]) -> list[Block] | None:
|
||||||
|
list_hit = look_like_list_item(para.text)
|
||||||
|
indented = para.paragraph.paragraph_format.left_indent or "\t" in (
|
||||||
|
para.paragraph.text or ""
|
||||||
|
)
|
||||||
|
if not (list_hit and indented):
|
||||||
|
return None
|
||||||
|
ordered, _ok, body = list_hit
|
||||||
|
self._flush_code(out)
|
||||||
|
self._append_list_item(ordered, body, out)
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _append_list_item(self, ordered: bool, body: str, out: list[Block]) -> None:
|
||||||
|
if self._list_buf is not None and self._list_ordered == ordered:
|
||||||
|
self._list_buf.append(body)
|
||||||
|
return
|
||||||
|
self._flush_list(out)
|
||||||
|
self._list_buf = [body]
|
||||||
|
self._list_ordered = ordered
|
||||||
|
|
||||||
|
def _as_plain_paragraph(self, para: _Para, out: list[Block]) -> list[Block]:
|
||||||
|
self._flush_code(out)
|
||||||
|
self._flush_list(out)
|
||||||
|
if not para.text and not paragraph_has_omml(para.element):
|
||||||
|
return out
|
||||||
|
spans = _inline_spans(para.paragraph)
|
||||||
|
# A paragraph holding nothing but OMML is a display equation
|
||||||
|
oms = find_omml(para.element)
|
||||||
|
if oms and not para.text.replace(" ", ""):
|
||||||
latex = omml_to_latex(oms[0])
|
latex = omml_to_latex(oms[0])
|
||||||
if latex:
|
if latex:
|
||||||
out.append(EquationBlock(latex=latex))
|
out.append(EquationBlock(latex=latex))
|
||||||
return out
|
return out
|
||||||
out.append(ParagraphBlock(spans=spans, style=style))
|
out.append(ParagraphBlock(spans=spans, style=para.style))
|
||||||
self._pending_caption = None
|
self._pending_caption = None
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user