BigUpdate 0.5.0
Python application / build (push) Has been cancelled

- add Local Render Mermaid
- add page-starе для указания смещения страниц
- add Гиперссылки в документе на списки литературы
This commit is contained in:
Igor20264
2026-09-07 09:54:33 +03:00
parent ac0aa33399
commit 1a5b35eb54
29 changed files with 4118 additions and 69 deletions
+4 -1
View File
@@ -32,9 +32,12 @@ if errorlevel 1 goto :pipfail
if not exist "md2gost\Template.docx" goto :notemplate if not exist "md2gost\Template.docx" goto :notemplate
echo [2/4] PlantUML jar, обязателен, вшивается в exe ... echo [2/4] PlantUML jar + Mermaid assets ...
python scripts\fetch_plantuml.py python scripts\fetch_plantuml.py
if not exist "md2gost\vendor\plantuml.jar" goto :nojar if not exist "md2gost\vendor\plantuml.jar" goto :nojar
if not exist "md2gost\diagrams\mermaid.min.js" (
python scripts\fetch_mermaid.py
)
echo [3/4] PyInstaller onefile, кэш build-fast ... echo [3/4] PyInstaller onefile, кэш build-fast ...
python -m PyInstaller --noconfirm %PYI_CLEAN% --workpath build-fast --distpath dist md2gost.fast.spec python -m PyInstaller --noconfirm %PYI_CLEAN% --workpath build-fast --distpath dist md2gost.fast.spec
+8 -2
View File
@@ -19,10 +19,16 @@ if errorlevel 1 goto :pipfail
if not exist "md2gost\Template.docx" goto :notemplate if not exist "md2gost\Template.docx" goto :notemplate
echo [2/4] PlantUML jar, вшивается в exe если скачается ... echo [2/4] PlantUML jar + Mermaid assets ...
python scripts\fetch_plantuml.py python scripts\fetch_plantuml.py
if not exist "md2gost\vendor\plantuml.jar" ( if not exist "md2gost\vendor\plantuml.jar" (
echo Предупреждение: plantuml.jar нет — в exe диаграммы пойдут через kroki.io. echo Предупреждение: plantuml.jar нет — в exe UML пойдёт через kroki.io.
)
if not exist "md2gost\diagrams\mermaid.min.js" (
python scripts\fetch_mermaid.py
)
if not exist "md2gost\diagrams\mermaid.min.js" (
echo Предупреждение: mermaid.min.js нет — браузерный Mermaid в exe без assets.
) )
echo [3/4] PyInstaller, один файл, без консоли; CLI подцепит консоль сам ... echo [3/4] PyInstaller, один файл, без консоли; CLI подцепит консоль сам ...
+6 -4
View File
@@ -29,10 +29,12 @@ python -m md2gost report.docx --check-pages # только проверка в
| `--table-continuation` | Основные | `word` (по умолчанию), `off`, `legacy`, `caption`, `soft` | | `--table-continuation` | Основные | `word` (по умолчанию), `off`, `legacy`, `caption`, `soft` |
| `--listing-continuation` | Основные | то же для листингов | | `--listing-continuation` | Основные | то же для листингов |
| `--table-repeat-header` | Галочка | шапка на фрагментах после word-split | | `--table-repeat-header` | Галочка | шапка на фрагментах после word-split |
| `--plantuml-jar` / `--kroki-url` | Настройки → Диаграммы | локальный рендер | | `--plantuml-jar` / `--kroki-url` | Настройки → Диаграммы | локальный рендер UML / запасной Kroki |
| `--diagram-fallback` | Диаграммы | `remote` / `local` / `off` | | `--diagram-fallback` | Диаграммы | `remote` / `local` / `off` (отключает только kroki.io) |
| `--diagram-format` | Диаграммы | `png` / `svg` | | `--diagram-format` | Диаграммы | `png` / `svg` |
| `--diagram-scale` | Диаграммы | качество PNG PlantUML (по умолчанию 2) | | `--diagram-scale` | Диаграммы | качество PNG PlantUML / локального Mermaid (по умолчанию 2) |
| `--page-start N` | Основные → Смещение страниц | начальный номер PAGE в теле; пусто = сквозной |
| `--install-chromium` | Диаграммы → «Скачать headless Chromium» | Chromium для Mermaid в `%LOCALAPPDATA%\md2gost` |
| `--schemes` | файл схем / Шаблоны UML | путь к `md2gost.schemes.json` | | `--schemes` | файл схем / Шаблоны UML | путь к `md2gost.schemes.json` |
| `--styles` | Настройки → Файлы → Стили JSON | оверлей `md2gost.styles.json` (также рядом с `.md`); см. [styles.md](styles.md) | | `--styles` | Настройки → Файлы → Стили JSON | оверлей `md2gost.styles.json` (также рядом с `.md`); см. [styles.md](styles.md) |
| `--syntax-highlighting` | (CLI) | подсветка в листингах | | `--syntax-highlighting` | (CLI) | подсветка в листингах |
@@ -45,7 +47,7 @@ DOCX не умеет сам писать «Продолжение Таблицы
## Меню GUI ## Меню GUI
- **Настройки → Файлы** — шаблон, титул, задание, стили JSON, выходной путь - **Настройки → Файлы** — шаблон, титул, задание, стили JSON, выходной путь
- **Настройки → Диаграммы** — PlantUML, Kroki, формат, кэш includes - **Настройки → Диаграммы** — PlantUML, Mermaid (Chromium), Kroki, формат, кэш includes
- **Шаблоны UML** — редактор `md2gost.schemes.json` - **Шаблоны UML** — редактор `md2gost.schemes.json`
- **Справка → Инструкция / Документация / Схемы / Промпт для ИИ** - **Справка → Инструкция / Документация / Схемы / Промпт для ИИ**
(Документация — встроенный просмотр `docs/*.md`, вшито в exe) (Документация — встроенный просмотр `docs/*.md`, вшито в exe)
+1 -1
View File
@@ -115,7 +115,7 @@ $$
## Источники ## Источники
В тексте: `[1]`, `[2, 3]` (для ВКР — `[1.5]`). В тексте: `[1]`, `[2, 3]` (для ВКР — `[1.5]`). В DOCX номера становятся ссылками на пункты списка источников.
В списке: В списке:
+9 -4
View File
@@ -1,6 +1,6 @@
# Схемы и диаграммы # Схемы и диаграммы
В отчёте блок кода с языком диаграммы превращается в **Рисунок** (PNG или SVG). PNG руками экспортировать не нужно: md2gost вызывает PlantUML (jar / Kroki) или Kroki для Mermaid. В отчёте блок кода с языком диаграммы превращается в **Рисунок** (PNG или SVG). PNG руками экспортировать не нужно: md2gost вызывает PlantUML (jar / Kroki) или локальный Mermaid (браузер / QuickJS), иначе Kroki.
## Синтаксис в markdown ## Синтаксис в markdown
@@ -31,7 +31,7 @@ Rel(user, app, "логин")
|---------|----------------| |---------|----------------|
| `` ```uml `` / `` ```plantuml `` | Сырой PlantUML (`@startuml`…`@enduml`) | | `` ```uml `` / `` ```plantuml `` | Сырой PlantUML (`@startuml`…`@enduml`) |
| `` ```uml-<id> `` / `` ```<id> `` | Схема из `md2gost.schemes.json` (обёртка и `!include` добавляются сами) | | `` ```uml-<id> `` / `` ```<id> `` | Схема из `md2gost.schemes.json` (обёртка и `!include` добавляются сами) |
| `` ```mermaid `` / `` ```mmd `` | Mermaid через Kroki | | `` ```mermaid `` / `` ```mmd `` | Mermaid: браузер / QuickJS / Kroki |
| `` ```bpmn `` / `` ```uml-bpmn `` | BPMN 2.0 (макросы библиотеки md2gost) | | `` ```bpmn `` / `` ```uml-bpmn `` | BPMN 2.0 (макросы библиотеки md2gost) |
**IDEF0 / DFD** конвертер не рисует — вставляйте готовый PNG как обычный Рисунок. **IDEF0 / DFD** конвертер не рисует — вставляйте готовый PNG как обычный Рисунок.
@@ -107,9 +107,14 @@ id схемы: латиница, цифры, `_`; начинается с бук
2. Локальный Kroki (`KROKI_URL` / `--kroki-url`, по умолчанию `http://localhost:8000`) 2. Локальный Kroki (`KROKI_URL` / `--kroki-url`, по умолчанию `http://localhost:8000`)
3. `https://kroki.io` при `--diagram-fallback remote` (по умолчанию) 3. `https://kroki.io` при `--diagram-fallback remote` (по умолчанию)
**Mermaid** — только Kroki (шаги 2–3). **Mermaid:**
Формат: `--diagram-format png` (по умолчанию) или `svg`. Масштаб рендера PlantUML: `--diagram-scale` (по умолчанию 2 — качество, размер на странице как при 1). 1. Системный Chrome / Edge (через Playwright, ничего не качаем)
2. Playwright Chromium в `%LOCALAPPDATA%\md2gost\ms-playwright` — кнопка «Скачать headless Chromium» или `md2gost --install-chromium`
3. QuickJS + mermaid.js (`mermaidx`) — офлайн без браузера
4. Локальный Kroki → `https://kroki.io` (если `--diagram-fallback remote`)
Формат: `--diagram-format png` (по умолчанию) или `svg`. Масштаб растра PlantUML / локального Mermaid: `--diagram-scale` (по умолчанию 2 — качество, размер на странице как при 1).
Кэш картинок: `{каталог_md}/.md2gost-cache/`. Кэш картинок: `{каталог_md}/.md2gost-cache/`.
Кэш includes: `md2gost.include-cache.json` + папка `include-cache/` (сброс в GUI: Настройки → Диаграммы / Шаблоны UML). Кэш includes: `md2gost.include-cache.json` + папка `include-cache/` (сброс в GUI: Настройки → Диаграммы / Шаблоны UML).
+15 -1
View File
@@ -44,8 +44,13 @@ datas += collect_data_files("docx")
# Explicit mpl-data so DejaVu + matplotlib font cache files are always in the exe. # Explicit mpl-data so DejaVu + matplotlib font cache files are always in the exe.
# hook-matplotlib.py also collects this; PyInstaller dedupes by dest path. # hook-matplotlib.py also collects this; PyInstaller dedupes by dest path.
datas += collect_data_files("matplotlib", includes=["mpl-data/**"]) datas += collect_data_files("matplotlib", includes=["mpl-data/**"])
for _pkg in ("mermaidx", "termaid", "quickjs", "quickjs_ng", "resvg_py", "playwright"):
try:
datas += collect_data_files(_pkg)
except Exception:
pass
for pkg in ("lxml", "freetype"): for pkg in ("lxml", "freetype", "quickjs", "resvg_py"):
try: try:
binaries += collect_dynamic_libs(pkg) binaries += collect_dynamic_libs(pkg)
except Exception: except Exception:
@@ -78,6 +83,15 @@ hiddenimports += [
"tkinter.filedialog", "tkinter.filedialog",
"tkinter.messagebox", "tkinter.messagebox",
"tkinter.ttk", "tkinter.ttk",
"mermaidx",
"mermaidx.diagram",
"mermaidx.engines",
"mermaidx.engines.quickjs_engine",
"termaid",
"quickjs",
"resvg_py",
"playwright",
"playwright.sync_api",
] ]
# Do NOT collect_submodules("pygments.lexers") here — hook-pygments.py already # Do NOT collect_submodules("pygments.lexers") here — hook-pygments.py already
# collects all lexers/formatters/styles once during Analysis. # collects all lexers/formatters/styles once during Analysis.
+15 -1
View File
@@ -27,8 +27,13 @@ datas += collect_data_files("lxml")
datas += collect_data_files("latex2mathml") datas += collect_data_files("latex2mathml")
datas += collect_data_files("docx") datas += collect_data_files("docx")
datas += collect_data_files("matplotlib", includes=["mpl-data/**"]) datas += collect_data_files("matplotlib", includes=["mpl-data/**"])
for _pkg in ("mermaidx", "termaid", "quickjs", "quickjs_ng", "resvg_py", "playwright"):
try:
datas += collect_data_files(_pkg)
except Exception:
pass
for pkg in ("lxml", "freetype"): for pkg in ("lxml", "freetype", "quickjs", "resvg_py"):
try: try:
binaries += collect_dynamic_libs(pkg) binaries += collect_dynamic_libs(pkg)
except Exception: except Exception:
@@ -61,6 +66,15 @@ hiddenimports += [
"tkinter.filedialog", "tkinter.filedialog",
"tkinter.messagebox", "tkinter.messagebox",
"tkinter.ttk", "tkinter.ttk",
"mermaidx",
"mermaidx.diagram",
"mermaidx.engines",
"mermaidx.engines.quickjs_engine",
"termaid",
"quickjs",
"resvg_py",
"playwright",
"playwright.sync_api",
] ]
hiddenimports += collect_submodules("pygments.lexers") hiddenimports += collect_submodules("pygments.lexers")
hiddenimports += collect_submodules("marko") hiddenimports += collect_submodules("marko")
+11 -4
View File
@@ -104,7 +104,7 @@ python -m md2gost report.md -o report.docx --toc manual
| Листинг | `%id Подпись` перед code fence | | Листинг | `%id Подпись` перед code fence |
| Диаграмма UML / Mermaid / схемы | `%id Подпись` + ````uml` / ````uml-c4` / ````bpmn` / ````mermaid` → Рисунок; `+listing` — ещё и Листинг. Схемы в `md2gost.schemes.json`. IDEF0 нет | | Диаграмма UML / Mermaid / схемы | `%id Подпись` + ````uml` / ````uml-c4` / ````bpmn` / ````mermaid` → Рисунок; `+listing` — ещё и Листинг. Схемы в `md2gost.schemes.json`. IDEF0 нет |
| Формула | `%eq1` + `$$…$$` + `@Формула:eq1` (номер только при ссылке) | | Формула | `%eq1` + `$$…$$` + `@Формула:eq1` (номер только при ссылке) |
| Источник | `[1]` в тексте; `[1]: …` в списке | | Источник | `[1]` в тексте (кликабельная ссылка на пункт списка); `[1]: …` в списке |
| Разрыв страницы | `---` на отдельной строке + `--hr-pagebreak` (по умолчанию `---` игнорируется) | | Разрыв страницы | `---` на отдельной строке + `--hr-pagebreak` (по умолчанию `---` игнорируется) |
### Таблицы со склеиванием ### Таблицы со склеиванием
@@ -208,7 +208,7 @@ Rel(user, app, "логин")
``` ```
```` ````
URL в `includes` схемы скачиваются в кэш (`md2gost.include-cache.json` + папка `include-cache/`). CLI: `--schemes path.json`. **BPMN 2.0** — оградка ````bpmn` / ````uml-bpmn` (макросы `Start`, `UserTask`, `XOR`, `Flow`, `Pool`…; библиотека `diagrams/BPMN.puml`). **Mermaid** — ````mermaid` / ````mmd` через тот же Kroki (свой `--kroki-url` или kroki.io); jar не используется. **IDEF0** конвертер не рисует — вставляйте готовый PNG. URL в `includes` схемы скачиваются в кэш (`md2gost.include-cache.json` + папка `include-cache/`). CLI: `--schemes path.json`. **BPMN 2.0** — оградка ````bpmn` / ````uml-bpmn` (макросы `Start`, `UserTask`, `XOR`, `Flow`, `Pool`…; библиотека `diagrams/BPMN.puml`). **Mermaid** — ````mermaid` / ````mmd`: системный браузер → Playwright Chromium → QuickJS (`mermaidx`) → Kroki (свой `--kroki-url` или kroki.io). **IDEF0** конвертер не рисует — вставляйте готовый PNG.
```bpmn ```bpmn
StartMessage(s, "заявка") StartMessage(s, "заявка")
@@ -243,9 +243,14 @@ DefaultFlow(gw, e_no)
2. Локальный Kroki (`KROKI_URL` / `--kroki-url`, по умолчанию `http://localhost:8000`) 2. Локальный Kroki (`KROKI_URL` / `--kroki-url`, по умолчанию `http://localhost:8000`)
3. Интернет `https://kroki.io` при `--diagram-fallback remote` (по умолчанию) 3. Интернет `https://kroki.io` при `--diagram-fallback remote` (по умолчанию)
Mermaid всегда идёт через Kroki (шаги 2–3). Mermaid:
Формат в Word: `--diagram-format png` (по умолчанию; PlantUML рендерится с `--diagram-scale`, по умолчанию 2 — только качество, размер на странице как при 1) или `svg` — вектор через `svgBlip` + PNG-запасной (Word 2016+; LibreOffice покажет растр). 1. Системный Chrome / Edge (Playwright channel, ничего не тащим)
2. Playwright Chromium в `%LOCALAPPDATA%\md2gost\ms-playwright` — GUI «Скачать headless Chromium» или `md2gost --install-chromium`
3. QuickJS + mermaid.js (`mermaidx`) — офлайн без браузера
4. Локальный Kroki → `https://kroki.io` при `--diagram-fallback remote`
Формат в Word: `--diagram-format png` (по умолчанию; PlantUML и локальный Mermaid с `--diagram-scale`, по умолчанию 2 — только качество, размер на странице как при 1) или `svg` — вектор через `svgBlip` + PNG-запасной (Word 2016+; LibreOffice покажет растр).
Широкие схемы/таблицы: в подписи флаг `+landscape` — отдельная альбомная A4-страница, затем снова книжная. Широкие схемы/таблицы: в подписи флаг `+landscape` — отдельная альбомная A4-страница, затем снова книжная.
@@ -255,6 +260,8 @@ Mermaid всегда идёт через Kroki (шаги 2–3).
```bash ```bash
python scripts/fetch_plantuml.py python scripts/fetch_plantuml.py
python scripts/fetch_mermaid.py
python -m md2gost --install-chromium
python -m md2gost report.md -o report.docx --diagram-fallback local python -m md2gost report.md -o report.docx --diagram-fallback local
python -m md2gost report.md -o report.docx --diagram-format svg python -m md2gost report.md -o report.docx --diagram-format svg
``` ```
+49 -2
View File
@@ -147,7 +147,7 @@ def build_parser() -> ArgumentParser:
"--diagram-fallback", "--diagram-fallback",
choices=["local", "remote", "off"], choices=["local", "remote", "off"],
default="remote", default="remote",
help="Если локальный рендер UML недоступен: remote (kroki.io), local (ошибка), off", help="Если локальный рендер недоступен: remote (kroki.io), local (ошибка), off",
) )
parser.add_argument( parser.add_argument(
"--diagram-format", "--diagram-format",
@@ -159,7 +159,28 @@ def build_parser() -> ArgumentParser:
"--diagram-scale", "--diagram-scale",
type=float, type=float,
default=2.0, default=2.0,
help="Масштаб рендера PlantUML PNG (качество); размер на странице как при 1. По умолчанию 2", help=(
"Масштаб растра PlantUML / локального Mermaid (качество); "
"размер на странице как при 1. По умолчанию 2"
),
)
parser.add_argument(
"--page-start",
type=int,
default=None,
metavar="N",
help=(
"Смещение / начальный номер страницы в колонтитуле тела документа. "
"Пусто — сквозная нумерация Word; например 1 — тело начинается с 1"
),
)
parser.add_argument(
"--install-chromium",
action="store_true",
help=(
"Скачать headless Chromium для локального Mermaid "
"в %%LOCALAPPDATA%%\\md2gost\\ms-playwright (и выйти)"
),
) )
parser.add_argument( parser.add_argument(
"--schemes", "--schemes",
@@ -206,6 +227,11 @@ def request_from_args(args) -> ConvertRequest:
diagram_scale=float(args.diagram_scale), diagram_scale=float(args.diagram_scale),
schemes_path=args.schemes_path, schemes_path=args.schemes_path,
styles_path=getattr(args, "styles_path", None), styles_path=getattr(args, "styles_path", None),
page_number_start=(
int(args.page_start)
if getattr(args, "page_start", None) is not None and int(args.page_start) >= 1
else None
),
debug=args.debug, debug=args.debug,
open_when_done=bool(args.debug), open_when_done=bool(args.debug),
) )
@@ -218,6 +244,8 @@ def _argv_needs_console(argv: list[str]) -> bool:
return False return False
if "-h" in args or "--help" in args: if "-h" in args or "--help" in args:
return True return True
if "--install-chromium" in args:
return True
if "--gui" in args: if "--gui" in args:
return False return False
return any(not a.startswith("-") for a in args) return any(not a.startswith("-") for a in args)
@@ -247,6 +275,25 @@ def main():
_enable_windows_console() _enable_windows_console()
parser = build_parser() parser = build_parser()
args = parser.parse_args() args = parser.parse_args()
if getattr(args, "install_chromium", False):
from .mermaid_renderer import (
install_playwright_chromium,
playwright_available,
playwright_browsers_dir,
)
if not playwright_available():
print("playwright не установлен. pip install playwright", file=sys.stderr)
sys.exit(1)
print(f"Устанавливаю Chromium → {playwright_browsers_dir()}")
ok = install_playwright_chromium()
if ok:
print("Готово.")
sys.exit(0)
print("Не удалось установить Chromium.", file=sys.stderr)
sys.exit(1)
req = request_from_args(args) req = request_from_args(args)
if should_launch_gui(args.filename, args.gui): if should_launch_gui(args.filename, args.gui):
+5
View File
@@ -16,6 +16,11 @@ CITE_RE = re.compile(
) )
def biblio_bookmark_name(key: str) -> str:
"""Word bookmark name for bibliography key (dots → underscores)."""
return "biblio_" + str(key).replace(".", "_")
@dataclass @dataclass
class BiblioEntry: class BiblioEntry:
key: str key: str
+4 -1
View File
@@ -41,13 +41,16 @@ class Bibliography(Renderable):
@staticmethod @staticmethod
def _make_entry(parent: Parented, e: BiblioEntry) -> Paragraph: def _make_entry(parent: Parented, e: BiblioEntry) -> Paragraph:
from .bibliography import biblio_bookmark_name
p = Paragraph(parent) p = Paragraph(parent)
try: try:
p.style = "Bibliography" p.style = "Bibliography"
except KeyError: except KeyError:
pass pass
# Avoid hyphen-splitting URLs: put whole entry in one run via direct text # Avoid hyphen-splitting URLs: put whole entry in one run via direct text
run = p._docx_paragraph.add_run(f"{e.key}. {e.text}") p._docx_paragraph.add_run(f"{e.key}. {e.text}")
p.wrap_with_bookmark(biblio_bookmark_name(e.key))
return p return p
def render(self, previous_rendered: RenderedInfo, layout_state: LayoutState) -> Generator[ def render(self, previous_rendered: RenderedInfo, layout_state: LayoutState) -> Generator[
+122 -22
View File
@@ -94,6 +94,13 @@ 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)
try:
from .mermaid_renderer import ensure_playwright_browsers_env, reset_mermaid_session
ensure_playwright_browsers_env()
reset_mermaid_session()
except Exception:
pass
fmt: DiagramFormat = "svg" if diagram_format == "svg" else "png" fmt: DiagramFormat = "svg" if diagram_format == "svg" else "png"
scale = DEFAULT_DIAGRAM_SCALE if diagram_scale is None else float(diagram_scale) scale = DEFAULT_DIAGRAM_SCALE if diagram_scale is None else float(diagram_scale)
if scale < 1: if scale < 1:
@@ -128,13 +135,19 @@ def _cache_root(cache_dir: str | None = None) -> Path:
return path return path
def _cache_digest(source: str) -> str: def _cache_digest(source: str, *, cache_prefix: str = "") -> str:
return hashlib.sha256(source.encode("utf-8")).hexdigest()[:24] payload = (cache_prefix + source) if cache_prefix else source
return hashlib.sha256(payload.encode("utf-8")).hexdigest()[:24]
def _cache_paths(source: str, cache_dir: str | None = None) -> tuple[Path, Path]: def _cache_paths(
source: str,
cache_dir: str | None = None,
*,
cache_prefix: str = "",
) -> tuple[Path, Path]:
root = _cache_root(cache_dir) root = _cache_root(cache_dir)
digest = _cache_digest(source) digest = _cache_digest(source, cache_prefix=cache_prefix)
return root / f"{digest}.png", root / f"{digest}.svg" return root / f"{digest}.png", root / f"{digest}.svg"
@@ -235,7 +248,14 @@ def diagram_engine_status(explicit_jar: str | None = None) -> str:
"PlantUML: файла нет. Нажмите «Скачать PlantUML» (нужна Java) " "PlantUML: файла нет. Нажмите «Скачать PlantUML» (нужна Java) "
"или оставьте как есть — картинки нарисует интернет (kroki.io)." "или оставьте как есть — картинки нарисует интернет (kroki.io)."
) )
return java_line + "\n" + jar_line lines = java_line + "\n" + jar_line
try:
from .mermaid_renderer import mermaid_engine_status
lines = lines + "\n" + mermaid_engine_status()
except Exception as exc:
lines = lines + f"\nMermaid: статус недоступен ({exc})"
return lines
def _ensure_plantuml_png_scale(source: str, scale: float = DEFAULT_DIAGRAM_SCALE) -> str: def _ensure_plantuml_png_scale(source: str, scale: float = DEFAULT_DIAGRAM_SCALE) -> str:
@@ -394,7 +414,32 @@ def render_diagram(
prepared = _ensure_plantuml_png_scale(prepared, scale) prepared = _ensure_plantuml_png_scale(prepared, scale)
if prepared != before: if prepared != before:
pixel_scale = scale pixel_scale = scale
out_png, out_svg_path = _cache_paths(prepared, cache_dir) # Mermaid: pixel_scale set only after successful local hi-res render (not Kroki).
cache_prefix = ""
if kroki_type == "mermaid":
try:
from .mermaid_renderer import CACHE_KEY_PREFIX
cache_prefix = CACHE_KEY_PREFIX
except Exception:
cache_prefix = "mmd-local-v4\n"
# Mermaid local hi-res uses a scale-specific cache key; Kroki uses unscaled key.
local_prefix = cache_prefix
if kroki_type == "mermaid" and scale > 1:
local_prefix = f"{cache_prefix}scale={scale:g}\n"
out_png, out_svg_path = _cache_paths(
prepared, cache_dir, cache_prefix=local_prefix if kroki_type == "mermaid" else cache_prefix,
)
# Kroki / remote paths (mermaid): never share the scaled local key.
kroki_png, kroki_svg_path = out_png, out_svg_path
if kroki_type == "mermaid":
kroki_png, kroki_svg_path = _cache_paths(
prepared, cache_dir, cache_prefix=cache_prefix,
)
fmt: DiagramFormat = ( fmt: DiagramFormat = (
diagram_format diagram_format
if diagram_format is not None if diagram_format is not None
@@ -402,29 +447,57 @@ def render_diagram(
) )
want_svg = fmt == "svg" want_svg = fmt == "svg"
svg_target = out_svg_path if want_svg else None svg_target = out_svg_path if want_svg else None
kroki_svg_target = kroki_svg_path if want_svg else None
if out_png.is_file() and out_png.stat().st_size > 0: if out_png.is_file() and out_png.stat().st_size > 0:
has_svg = out_svg_path.is_file() and out_svg_path.stat().st_size > 0 has_svg = out_svg_path.is_file() and out_svg_path.stat().st_size > 0
if not want_svg or has_svg: if not want_svg or has_svg:
hit_scale = pixel_scale
if kroki_type == "mermaid" and scale > 1 and local_prefix != cache_prefix:
hit_scale = scale
return DiagramRenderResult( return DiagramRenderResult(
png_path=str(out_png), png_path=str(out_png),
svg_path=str(out_svg_path) if (want_svg and has_svg) else None, svg_path=str(out_svg_path) if (want_svg and has_svg) else None,
pixel_scale=pixel_scale, pixel_scale=hit_scale,
) )
# PNG cached but SVG missing in svg mode — fall through to fill SVG. # PNG cached but SVG missing in svg mode — fall through to fill SVG.
# Mermaid: also accept unscaled Kroki cache hit
if (
kroki_type == "mermaid"
and kroki_png != out_png
and kroki_png.is_file()
and kroki_png.stat().st_size > 0
):
has_svg = kroki_svg_path.is_file() and kroki_svg_path.stat().st_size > 0
if not want_svg or has_svg:
return DiagramRenderResult(
png_path=str(kroki_png),
svg_path=str(kroki_svg_path) if (want_svg and has_svg) else None,
pixel_scale=1.0,
)
jar = plantuml_jar if plantuml_jar is not None else _CONFIG.plantuml_jar jar = plantuml_jar if plantuml_jar is not None else _CONFIG.plantuml_jar
jar = resolve_plantuml_jar(jar or None, download=False) jar = resolve_plantuml_jar(jar or None, download=False)
local_kroki = kroki_url if kroki_url is not None else _CONFIG.kroki_url local_kroki = kroki_url if kroki_url is not None else _CONFIG.kroki_url
local_kroki = local_kroki or os.environ.get("KROKI_URL") or DEFAULT_KROKI_URL local_kroki = local_kroki or os.environ.get("KROKI_URL") or DEFAULT_KROKI_URL
mode: FallbackMode = fallback if fallback is not None else _CONFIG.fallback mode: FallbackMode = fallback if fallback is not None else _CONFIG.fallback
def _result() -> DiagramRenderResult: def _result(
png: Path | None = None,
svg_path: Path | None = None,
*,
pscale: float | None = None,
) -> DiagramRenderResult:
use_png = png or out_png
use_svg_path = svg_path if svg_path is not None else out_svg_path
svg = None svg = None
if want_svg and out_svg_path.is_file() and out_svg_path.stat().st_size > 0: if want_svg and use_svg_path.is_file() and use_svg_path.stat().st_size > 0:
svg = str(out_svg_path) svg = str(use_svg_path)
return DiagramRenderResult( return DiagramRenderResult(
png_path=str(out_png), svg_path=svg, pixel_scale=pixel_scale, png_path=str(use_png),
svg_path=svg,
pixel_scale=pixel_scale if pscale is None else pscale,
) )
# 1) PlantUML jar — only for plantuml type # 1) PlantUML jar — only for plantuml type
@@ -432,15 +505,40 @@ def render_diagram(
if _render_plantuml_jar(prepared, out_png, jar or "", out_svg=svg_target): if _render_plantuml_jar(prepared, out_png, jar or "", out_svg=svg_target):
return _result() return _result()
# 1b) Mermaid local: system browser → Playwright Chromium → QuickJS
if kroki_type == "mermaid":
try:
from .mermaid_renderer import write_mermaid_outputs
if write_mermaid_outputs(
prepared, out_png, svg_target, scale=scale,
):
return _result(pscale=scale if scale > 1 else 1.0)
except Exception as exc:
_log.warning("Mermaid local render failed: %s", exc)
# 2) Local Kroki # 2) Local Kroki
if _render_kroki_pair(prepared, kroki_type, local_kroki, out_png, svg_target): if _render_kroki_pair(prepared, kroki_type, local_kroki, kroki_png, kroki_svg_target):
return _result() if kroki_type == "mermaid":
return _result(kroki_png, kroki_svg_path, pscale=1.0)
return _result(kroki_png, kroki_svg_path)
def _png_fallback_or_raise(message: str) -> DiagramRenderResult: def _png_fallback_or_raise(message: str) -> DiagramRenderResult:
if out_png.is_file() and out_png.stat().st_size > 0: for candidate in (out_png, kroki_png):
if candidate.is_file() and candidate.stat().st_size > 0:
_log.warning("Диаграмма: SVG недоступен, вставляю только PNG") _log.warning("Диаграмма: SVG недоступен, вставляю только PNG")
pscale = pixel_scale
if (
kroki_type == "mermaid"
and candidate == out_png
and scale > 1
and local_prefix != cache_prefix
):
pscale = scale
elif kroki_type == "mermaid" and candidate == kroki_png:
pscale = 1.0
return DiagramRenderResult( return DiagramRenderResult(
png_path=str(out_png), svg_path=None, pixel_scale=pixel_scale, png_path=str(candidate), svg_path=None, pixel_scale=pscale,
) )
raise RuntimeError(message) raise RuntimeError(message)
@@ -451,8 +549,8 @@ def render_diagram(
"(задайте --plantuml-jar или KROKI_URL; remote fallback отключён)" "(задайте --plantuml-jar или KROKI_URL; remote fallback отключён)"
) )
return _png_fallback_or_raise( return _png_fallback_or_raise(
"Не удалось отрендерить диаграмму через локальный Kroki " "Не удалось отрендерить диаграмму локально "
"(задайте --kroki-url / KROKI_URL; remote fallback отключён)" "(браузер / QuickJS / --kroki-url; remote fallback отключён)"
) )
if mode == "local": if mode == "local":
if kroki_type == "plantuml": if kroki_type == "plantuml":
@@ -461,8 +559,8 @@ def render_diagram(
"(Java+plantuml.jar или локальный Kroki)" "(Java+plantuml.jar или локальный Kroki)"
) )
return _png_fallback_or_raise( return _png_fallback_or_raise(
"Локальный Kroki недоступен для диаграммы " "Локальный рендер Mermaid недоступен "
f"(тип {kroki_type}; задайте работающий --kroki-url)" f"(браузер / Chromium / QuickJS / Kroki; тип {kroki_type})"
) )
# 3) Remote fallback # 3) Remote fallback
@@ -470,11 +568,13 @@ def render_diagram(
"Диаграмма: локальный рендер недоступен, использую remote %s", "Диаграмма: локальный рендер недоступен, использую remote %s",
REMOTE_KROKI_URL, REMOTE_KROKI_URL,
) )
if _render_kroki_pair(prepared, kroki_type, REMOTE_KROKI_URL, out_png, svg_target): if _render_kroki_pair(prepared, kroki_type, REMOTE_KROKI_URL, kroki_png, kroki_svg_target):
return _result() if kroki_type == "mermaid":
return _result(kroki_png, kroki_svg_path, pscale=1.0)
return _result(kroki_png, kroki_svg_path)
return _png_fallback_or_raise( return _png_fallback_or_raise(
"Не удалось отрендерить диаграмму (PlantUML/Kroki)" "Не удалось отрендерить диаграмму (PlantUML/Mermaid/Kroki)"
) )
+2314
View File
File diff suppressed because one or more lines are too long
+84
View File
@@ -0,0 +1,84 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<title>md2gost mermaid</title>
<script src="mermaid.min.js"></script>
<style>
html, body { margin: 0; padding: 0; background: #fff; }
#out { display: inline-block; }
</style>
</head>
<body>
<div id="out"></div>
<script>
// Browser: htmlLabels:true — корректные боксы под кириллицу; PNG = screenshot.
mermaid.initialize({
startOnLoad: false,
securityLevel: "loose",
flowchart: {
htmlLabels: true,
wrappingWidth: 320,
subGraphTitleMargin: { top: 12, bottom: 16 },
padding: 20,
},
themeVariables: {
fontFamily: '"Segoe UI", "DejaVu Sans", Arial, sans-serif',
},
});
/** Put subgraph titles above node rows and paint them on top (Mermaid LR quirk). */
function fixClusterLabels(root) {
root.querySelectorAll("g.cluster").forEach((cluster) => {
const label = cluster.querySelector(".cluster-label");
const rect = cluster.querySelector(":scope > rect, rect");
if (!label || !rect) return;
const fo = label.querySelector("foreignObject");
const rw = parseFloat(rect.getAttribute("width") || "0");
const rx = parseFloat(rect.getAttribute("x") || "0");
const ry = parseFloat(rect.getAttribute("y") || "0");
if (fo && rw > 0) {
const maxW = Math.max(160, rw - 24);
fo.setAttribute("width", String(maxW));
const inner = fo.querySelector("div");
if (inner) {
inner.style.maxWidth = maxW + "px";
inner.style.width = maxW + "px";
inner.style.whiteSpace = "normal";
}
}
let labelH = 28;
try {
const bb = label.getBBox();
if (bb && bb.height > 0) labelH = bb.height;
else if (fo) labelH = parseFloat(fo.getAttribute("height") || "48") || 48;
} catch (e) {
if (fo) labelH = parseFloat(fo.getAttribute("height") || "48") || 48;
}
let labelW = 200;
try {
const bb = label.getBBox();
if (bb && bb.width > 0) labelW = bb.width;
else if (fo) labelW = parseFloat(fo.getAttribute("width") || "200") || 200;
} catch (e) {
if (fo) labelW = parseFloat(fo.getAttribute("width") || "200") || 200;
}
const x = rx + Math.max(0, (rw - labelW) / 2);
const y = ry + 6;
label.setAttribute("transform", "translate(" + x + ", " + y + ")");
cluster.appendChild(label);
});
}
window.__md2gostRender = async function (code) {
const id = "m" + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
const out = await mermaid.render(id, code);
const box = document.getElementById("out");
box.innerHTML = out.svg;
fixClusterLabels(box);
const svg = box.querySelector("svg");
return svg ? svg.outerHTML : out.svg;
};
</script>
</body>
</html>
+92 -10
View File
@@ -65,7 +65,7 @@ TABLE_LABELS = {
} }
FALLBACK_LABELS = { FALLBACK_LABELS = {
"remote": "Авто: локально, иначе интернет (kroki.io)", "remote": "Авто: локально, иначе интернет (kroki.io)",
"local": "Только этот компьютер (нужна Java)", "local": "Только этот компьютер (без kroki.io)",
"off": "Не рисовать диаграммы", "off": "Не рисовать диаграммы",
} }
FORMAT_LABELS = { FORMAT_LABELS = {
@@ -257,6 +257,7 @@ class Md2GostApp:
self.plantuml_var = tk.StringVar() self.plantuml_var = tk.StringVar()
self.kroki_var = tk.StringVar() self.kroki_var = tk.StringVar()
self.scale_var = tk.StringVar(value="2") self.scale_var = tk.StringVar(value="2")
self.page_start_var = tk.StringVar(value="")
self.highlight_var = tk.BooleanVar(value=False) self.highlight_var = tk.BooleanVar(value=False)
self.debug_var = tk.BooleanVar(value=False) self.debug_var = tk.BooleanVar(value=False)
self.output_var.trace_add("write", self._on_output_edited) self.output_var.trace_add("write", self._on_output_edited)
@@ -351,8 +352,8 @@ class Md2GostApp:
win = tk.Toplevel(self.root) win = tk.Toplevel(self.root)
win.title("Настройки — Диаграммы") win.title("Настройки — Диаграммы")
win.configure(bg=PAPER) win.configure(bg=PAPER)
win.minsize(560, 420) win.minsize(560, 480)
win.geometry("680x520") win.geometry("700x600")
win.transient(self.root) win.transient(self.root)
extra = ttk.Frame(win, padding=12) extra = ttk.Frame(win, padding=12)
extra.pack(fill=tk.BOTH, expand=True) extra.pack(fill=tk.BOTH, expand=True)
@@ -363,7 +364,8 @@ class Md2GostApp:
text=( text=(
"Блоки ```uml / ```uml-c4 / ```bpmn / ```mermaid сами станут рисунками.\n" "Блоки ```uml / ```uml-c4 / ```bpmn / ```mermaid сами станут рисунками.\n"
"Схемы редактируются в меню «Шаблоны UML» (файл md2gost.schemes.json).\n" "Схемы редактируются в меню «Шаблоны UML» (файл md2gost.schemes.json).\n"
"UML: сначала PlantUML + Java, иначе интернет (kroki.io). Mermaid — только через Kroki." "UML: PlantUML + Java, иначе Kroki. Mermaid: системный браузер → "
"Chromium → QuickJS → Kroki (kroki.io остаётся запасным)."
), ),
style="Hint.TLabel", style="Hint.TLabel",
wraplength=640, wraplength=640,
@@ -375,16 +377,25 @@ class Md2GostApp:
status_row.grid(row=1, column=0, columnspan=2, sticky="ew", pady=(0, 8)) status_row.grid(row=1, column=0, columnspan=2, sticky="ew", pady=(0, 8))
status_row.columnconfigure(0, weight=1) status_row.columnconfigure(0, weight=1)
self.diag_status = ttk.Label(status_row, text="", style="Hint.TLabel", wraplength=480, justify=tk.LEFT) self.diag_status = ttk.Label(status_row, text="", style="Hint.TLabel", wraplength=480, justify=tk.LEFT)
self.diag_status.grid(row=0, column=0, sticky="w") self.diag_status.grid(row=0, column=0, sticky="w", rowspan=2)
btn_dl = ttk.Button(status_row, text="Скачать PlantUML", command=self._download_plantuml) btn_dl = ttk.Button(status_row, text="Скачать PlantUML", command=self._download_plantuml)
btn_dl.grid(row=0, column=1, sticky="e", padx=(8, 0)) btn_dl.grid(row=0, column=1, sticky="e", padx=(8, 0))
_tip(btn_dl, "Скачать plantuml.jar для локальной отрисовки (нужна Java)") _tip(btn_dl, "Скачать plantuml.jar для локальной отрисовки (нужна Java)")
btn_cr = ttk.Button(
status_row, text="Скачать headless Chromium", command=self._download_chromium,
)
btn_cr.grid(row=1, column=1, sticky="e", padx=(8, 0), pady=(4, 0))
_tip(
btn_cr,
"Скачать Chromium для Mermaid в %LOCALAPPDATA%\\md2gost "
"(если нет системного Edge/Chrome)",
)
self._combo( self._combo(
extra, 2, "Как рисовать", extra, 2, "Как рисовать",
self.fallback_var, self.fallback_var,
_combo_values(FALLBACK_LABELS, ("remote", "local", "off")), _combo_values(FALLBACK_LABELS, ("remote", "local", "off")),
tip="Локально (PlantUML+Java), через интернет или не рисовать", tip="Локально (PlantUML / браузер / QuickJS), через интернет или не рисовать",
) )
self._combo( self._combo(
extra, 3, "Формат схем", extra, 3, "Формат схем",
@@ -393,12 +404,12 @@ class Md2GostApp:
tip="PNG везде; SVG — вектор в Word 2016+ (PNG рядом как запасной)", tip="PNG везде; SVG — вектор в Word 2016+ (PNG рядом как запасной)",
) )
ttk.Label(extra, text="Scale PNG (PlantUML)").grid(row=4, column=0, sticky="w", padx=(0, 8), pady=3) ttk.Label(extra, text="Scale PNG").grid(row=4, column=0, sticky="w", padx=(0, 8), pady=3)
scale_row = ttk.Frame(extra) scale_row = ttk.Frame(extra)
scale_row.grid(row=4, column=1, sticky="w", pady=3) scale_row.grid(row=4, column=1, sticky="w", pady=3)
scale_entry = ttk.Entry(scale_row, textvariable=self.scale_var, width=8) scale_entry = ttk.Entry(scale_row, textvariable=self.scale_var, width=8)
scale_entry.pack(side=tk.LEFT) scale_entry.pack(side=tk.LEFT)
_tip(scale_entry, "Качество растра PlantUML; на странице размер как при 1") _tip(scale_entry, "Качество растра PlantUML / локального Mermaid; на странице размер как при 1")
ttk.Label( ttk.Label(
scale_row, scale_row,
text=" качество; на странице размер как при 1", text=" качество; на странице размер как при 1",
@@ -422,7 +433,10 @@ class Md2GostApp:
tip="Пусто — вшитый jar или скачанный автоматически") tip="Пусто — вшитый jar или скачанный автоматически")
ttk.Label( ttk.Label(
extra, extra,
text="Свой Kroki — только если у вас крутится свой сервер. Пусто = сначала localhost:8000, потом kroki.io.", text=(
"Свой Kroki — запасной путь (и для UML без Java). Пусто = localhost:8000, потом kroki.io. "
"Mermaid сначала рисуется локально (браузер / QuickJS)."
),
style="Hint.TLabel", style="Hint.TLabel",
wraplength=640, wraplength=640,
).grid(row=8, column=0, columnspan=2, sticky="w", pady=(6, 0)) ).grid(row=8, column=0, columnspan=2, sticky="w", pady=(6, 0))
@@ -822,8 +836,24 @@ class Md2GostApp:
self._combo(main, 4, "Продолжение листингов", self.listing_var, _combo_values(TABLE_LABELS, LISTING_CONTINUATION_MODES), self._combo(main, 4, "Продолжение листингов", self.listing_var, _combo_values(TABLE_LABELS, LISTING_CONTINUATION_MODES),
tip="То же для листингов; word требует Windows + Word + pywin32") tip="То же для листингов; word требует Windows + Word + pywin32")
ttk.Label(main, text="Смещение страниц").grid(row=5, column=0, sticky="w", padx=(0, 8), pady=3)
page_row = ttk.Frame(main)
page_row.grid(row=5, column=1, sticky="w", pady=3)
page_entry = ttk.Entry(page_row, textvariable=self.page_start_var, width=8)
page_entry.pack(side=tk.LEFT)
_tip(
page_entry,
"Начальный номер PAGE в колонтитуле тела. Пусто — сквозная нумерация Word; "
"например 1 — тело с 1 (титул/задание без номера)",
)
ttk.Label(
page_row,
text=" пусто = сквозной; 1 = тело с первой",
style="Hint.TLabel",
).pack(side=tk.LEFT)
flags = ttk.Frame(main) flags = ttk.Frame(main)
flags.grid(row=5, column=0, columnspan=2, sticky="w", pady=(8, 0)) flags.grid(row=6, column=0, columnspan=2, sticky="w", pady=(8, 0))
checks = [ checks = [
("Заменять «—» на «-»", self.emdash_var, "Типографское тире → дефис (методичка обычно требует «—»)"), ("Заменять «—» на «-»", self.emdash_var, "Типографское тире → дефис (методичка обычно требует «—»)"),
("--- → разрыв страницы", self.hr_pagebreak_var, "Строка --- станет page break в Word"), ("--- → разрыв страницы", self.hr_pagebreak_var, "Строка --- станет page break в Word"),
@@ -1260,6 +1290,45 @@ class Md2GostApp:
"Можно ничего не делать — диаграммы нарисует kroki.io.", "Можно ничего не делать — диаграммы нарисует kroki.io.",
) )
def _download_chromium(self) -> None:
from .mermaid_renderer import install_playwright_chromium, playwright_browsers_dir, playwright_available
if not playwright_available():
messagebox.showerror(
"md2gost",
"Пакет playwright не установлен.\n"
"Установите: pip install playwright\n"
"Mermaid всё равно может рисоваться через QuickJS (mermaidx) или Kroki.",
)
return
dest = playwright_browsers_dir()
self._append_log(f"Скачиваю headless Chromium → {dest}")
self.root.update_idletasks()
def work() -> None:
ok = install_playwright_chromium()
self.root.after(0, lambda: self._on_chromium_installed(ok, dest))
import threading
threading.Thread(target=work, daemon=True).start()
def _on_chromium_installed(self, ok: bool, dest) -> None:
self._refresh_diagram_status()
if ok:
messagebox.showinfo(
"md2gost",
f"Chromium установлен:\n{dest}\n\n"
"Будет использоваться для локального Mermaid, если нет системного Edge/Chrome.",
)
else:
messagebox.showerror(
"md2gost",
"Не удалось скачать Chromium.\n"
"Попробуйте: md2gost --install-chromium\n"
"Или оставьте как есть — Mermaid через QuickJS / kroki.io.",
)
def _pick_into(self, var: tk.StringVar, title: str, types) -> None: def _pick_into(self, var: tk.StringVar, title: str, types) -> None:
path = filedialog.askopenfilename(title=title, filetypes=types) path = filedialog.askopenfilename(title=title, filetypes=types)
if path: if path:
@@ -1300,6 +1369,8 @@ class Md2GostApp:
self.strict_var.set(bool(req.strict)) self.strict_var.set(bool(req.strict))
self.check_pages_var.set(bool(getattr(req, "check_pages", False))) self.check_pages_var.set(bool(getattr(req, "check_pages", False)))
self.table_repeat_header_var.set(bool(getattr(req, "table_repeat_header", False))) self.table_repeat_header_var.set(bool(getattr(req, "table_repeat_header", False)))
start = getattr(req, "page_number_start", None)
self.page_start_var.set("" if start is None else str(int(start)))
self.highlight_var.set(bool(req.syntax_highlighting)) self.highlight_var.set(bool(req.syntax_highlighting))
self.debug_var.set(bool(req.debug)) self.debug_var.set(bool(req.debug))
self._debug_next = bool(req.debug) self._debug_next = bool(req.debug)
@@ -1333,6 +1404,7 @@ class Md2GostApp:
check_only=self.check_only_var.get(), check_only=self.check_only_var.get(),
check_pages=self.check_pages_var.get(), check_pages=self.check_pages_var.get(),
table_repeat_header=self.table_repeat_header_var.get(), table_repeat_header=self.table_repeat_header_var.get(),
page_number_start=self._parse_page_start(),
strict=self.strict_var.get(), strict=self.strict_var.get(),
syntax_highlighting=self.highlight_var.get(), syntax_highlighting=self.highlight_var.get(),
plantuml_jar=self.plantuml_var.get().strip() or None, plantuml_jar=self.plantuml_var.get().strip() or None,
@@ -1345,6 +1417,16 @@ class Md2GostApp:
open_when_done=self.open_var.get(), open_when_done=self.open_var.get(),
) )
def _parse_page_start(self) -> int | None:
raw = (self.page_start_var.get() or "").strip()
if not raw:
return None
try:
value = int(raw)
except ValueError:
return None
return value if value >= 1 else None
def _parse_diagram_scale(self) -> float: def _parse_diagram_scale(self) -> float:
raw = (self.scale_var.get() or "2").strip().replace(",", ".") raw = (self.scale_var.get() or "2").strip().replace(",", ".")
try: try:
+9 -7
View File
@@ -13,7 +13,7 @@ USAGE_HELP = """md2gost — Markdown → DOCX (ТЗ МИРЭА / ГОСТ 7.32)
1. Перетащите .md в верхнюю область (или кликните по ней). 1. Перетащите .md в верхнюю область (или кликните по ней).
2. Выберите тип документа и параметры в блоке «Основные». 2. Выберите тип документа и параметры в блоке «Основные».
Шаблон / титул / задание — Настройки → Файлы. Шаблон / титул / задание — Настройки → Файлы.
PlantUML / Kroki — Настройки → Диаграммы. PlantUML / Mermaid / Kroki — Настройки → Диаграммы.
Свои UML-схемы — меню «Шаблоны UML». Свои UML-схемы — меню «Шаблоны UML».
3. Нажмите «Конвертировать». Документ сохранится рядом с исходником (или по пути «Выходной DOCX»). 3. Нажмите «Конвертировать». Документ сохранится рядом с исходником (или по пути «Выходной DOCX»).
Если файл уже есть и вы откажетесь перезаписывать — сохранится как имя_гггг-мм-дд-ЧЧ-ММ.docx. Если файл уже есть и вы откажетесь перезаписывать — сохранится как имя_гггг-мм-дд-ЧЧ-ММ.docx.
@@ -24,6 +24,7 @@ USAGE_HELP = """md2gost — Markdown → DOCX (ТЗ МИРЭА / ГОСТ 7.32)
Полезные галочки Полезные галочки
• «—» → «-» — заменить типографское тире на дефис (по умолчанию **выкл.**; методичка требует «—»). • «—» → «-» — заменить типографское тире на дефис (по умолчанию **выкл.**; методичка требует «—»).
• --- → разрыв страницы — по умолчанию выкл. (строка --- игнорируется). Вкл. — page break в Word. • --- → разрыв страницы — по умолчанию выкл. (строка --- игнорируется). Вкл. — page break в Word.
• Смещение страниц — начальный номер PAGE в теле (пусто = сквозной; 1 = тело с 1).
• Проверить по ТЗ — замечания по структуре, «рис.», источникам. • Проверить по ТЗ — замечания по структуре, «рис.», источникам.
• Проверить вёрстку в Word — полупустые страницы (эвристика, возможны ложные срабатывания; нужны Word + pywin32). • Проверить вёрстку в Word — полупустые страницы (эвристика, возможны ложные срабатывания; нужны Word + pywin32).
• Титул / задание — отдельные DOCX, вставляются перед телом отчёта. • Титул / задание — отдельные DOCX, вставляются перед телом отчёта.
@@ -93,7 +94,7 @@ CLI (тот же движок)
``` ```
Mermaid (через Kroki, свой URL или kroki.io): Mermaid (локально: браузер / QuickJS; иначе Kroki):
%seq Последовательность +listing %seq Последовательность +listing
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
@@ -104,7 +105,7 @@ CLI (тот же движок)
+landscape — отдельная альбомная страница (A4, повёрт на 90°) вокруг рисунка/таблицы, затем снова книжная. +landscape — отдельная альбомная страница (A4, повёрт на 90°) вокруг рисунка/таблицы, затем снова книжная.
Языки: uml, plantuml, mermaid / mmd, или uml-<id> / <id> из файла схем (c4, usecase, bpmn, …). Языки: uml, plantuml, mermaid / mmd, или uml-<id> / <id> из файла схем (c4, usecase, bpmn, …).
BPMN 2.0: оградка ```bpmn / ```uml-bpmn, макросы Start, UserTask, XOR, Flow, Pool. BPMN 2.0: оградка ```bpmn / ```uml-bpmn, макросы Start, UserTask, XOR, Flow, Pool.
Формат в Word: PNG по умолчанию (PlantUML рисуется ~2× для чёткости); --diagram-format svg — вектор + PNG-запасной (Word 2016+). Формат в Word: PNG по умолчанию (PlantUML и локальный Mermaid ~2× для чёткости); --diagram-format svg — вектор + PNG-запасной (Word 2016+).
IDEF0 конвертер не рисует — вставляйте готовый PNG как обычный Рисунок. IDEF0 конвертер не рисует — вставляйте готовый PNG как обычный Рисунок.
Подробнее — Справка → Схемы и раздел ниже в инструкции. Подробнее — Справка → Схемы и раздел ниже в инструкции.
@@ -114,7 +115,7 @@ IDEF0 конвертер не рисует — вставляйте готовы
См. @Формула:eq1 См. @Формула:eq1
Источники Источники
В тексте: [1] В тексте: [1] (в DOCX — ссылка на пункт списка)
В списке: [1]: Иванов И. И. Название. — М.: Наука, 2023. — 120 с. В списке: [1]: Иванов И. И. Название. — М.: Наука, 2023. — 120 с.
Разрыв страницы Разрыв страницы
@@ -157,7 +158,7 @@ SCHEMES_HELP = """СХЕМЫ ДИАГРАММ (PlantUML)
```uml-c4 — схема с id «c4» (то же, что ```c4) ```uml-c4 — схема с id «c4» (то же, что ```c4)
```uml-usecase — схема «usecase» ```uml-usecase — схема «usecase»
```bpmn / ```uml-bpmn — BPMN 2.0 (макросы в diagrams/BPMN.puml) ```bpmn / ```uml-bpmn — BPMN 2.0 (макросы в diagrams/BPMN.puml)
```mermaid / ```mmd — Mermaid через Kroki (не PlantUML) ```mermaid / ```mmd — Mermaid локально (браузер / Chromium / QuickJS) или через Kroki
Встроенные пресеты PlantUML: c4, c4context, c4component, usecase, bpmn. Встроенные пресеты PlantUML: c4, c4context, c4component, usecase, bpmn.
Пример Пример
@@ -225,8 +226,9 @@ BPMN 2.0 (```bpmn)
Рендер Рендер
UML: Java + plantuml.jar (локально) → иначе локальный Kroki → иначе kroki.io. UML: Java + plantuml.jar (локально) → иначе локальный Kroki → иначе kroki.io.
Mermaid: только Kroki (свой --kroki-url / localhost / kroki.io). Mermaid: системный Chrome/Edge → Playwright Chromium (кнопка / --install-chromium) →
Формат: PNG по умолчанию (PlantUML ~scale 2 для чёткости, размер на странице как при 1; --diagram-scale); --diagram-format svg — PNG + SVG (svgBlip в Word 2016+). QuickJS (mermaidx, офлайн) → локальный Kroki → kroki.io.
Формат: PNG по умолчанию (PlantUML / локальный Mermaid ~scale 2 для чёткости, размер на странице как при 1; --diagram-scale); --diagram-format svg — PNG + SVG (svgBlip в Word 2016+).
+landscape у %подписи — альбомная страница под широкий рисунок/таблицу. +landscape у %подписи — альбомная страница под широкий рисунок/таблицу.
""" """
+607
View File
@@ -0,0 +1,607 @@
"""Local Mermaid rendering: system browser → Playwright Chromium → QuickJS (mermaidx)."""
from __future__ import annotations
import logging
import os
import subprocess
import threading
from pathlib import Path
from typing import Literal
from . import package_dir
_log = logging.getLogger(__name__)
# Pinned assets next to PlantUML schemes.
MERMAID_JS_NAME = "mermaid.min.js"
MERMAID_PAGE_NAME = "mermaid_page.html"
CACHE_KEY_PREFIX = "mmd-local-v4\n"
# QuickJS/resvg cannot paint HTML foreignObject — use SVG <text> there only.
# Browser keeps htmlLabels:true (correct box sizing for Cyrillic) + PNG via screenshot.
_MERMAID_CONFIG_QUICKJS = {
"flowchart": {"htmlLabels": False},
"themeVariables": {
"fontFamily": '"Segoe UI", "DejaVu Sans", Arial, sans-serif',
},
}
_MERMAID_INIT_QUICKJS = (
"%%{init: {"
"'flowchart': {'htmlLabels': false}, "
"'themeVariables': {'fontFamily': '\"Segoe UI\", \"DejaVu Sans\", Arial, sans-serif'}"
"}}%%\n"
)
_MERMAID_INIT_BROWSER = (
"%%{init: {"
"'flowchart': {"
"'htmlLabels': true, "
"'wrappingWidth': 320, "
"'subGraphTitleMargin': {'top': 12, 'bottom': 16}, "
"'padding': 20"
"}, "
"'themeVariables': {'fontFamily': '\"Segoe UI\", \"DejaVu Sans\", Arial, sans-serif'}"
"}}%%\n"
)
def prepare_mermaid_source(source: str, *, html_labels: bool = True) -> str:
"""
Prepend %%{init}%%.
html_labels=True — browser (correct layout for Cyrillic, PNG via screenshot).
html_labels=False — QuickJS/resvg (SVG <text> so glyphs are not dropped).
"""
text = source.lstrip("\ufeff").strip()
if text.startswith("%%{init"):
return text
return (_MERMAID_INIT_BROWSER if html_labels else _MERMAID_INIT_QUICKJS) + text
# Prefer installed browsers; bundled Chromium is optional (button / --install-chromium).
_SYSTEM_CHANNELS = ("msedge", "chrome", "chromium")
_lock = threading.RLock()
_session: "_BrowserSession | None" = None
_UNSET = object()
_cached_system_channel: str | None | object = _UNSET
def diagrams_dir() -> Path:
return Path(package_dir()) / "diagrams"
def _windows_browser_exe(channel: str) -> Path | None:
"""Best-effort path probe so GUI status does not launch browsers."""
local = Path(os.environ.get("LOCALAPPDATA") or "")
pf = Path(os.environ.get("PROGRAMFILES") or r"C:\Program Files")
pf86 = Path(os.environ.get("PROGRAMFILES(X86)") or r"C:\Program Files (x86)")
candidates: list[Path] = []
if channel == "msedge":
candidates = [
pf / "Microsoft" / "Edge" / "Application" / "msedge.exe",
pf86 / "Microsoft" / "Edge" / "Application" / "msedge.exe",
]
elif channel == "chrome":
candidates = [
pf / "Google" / "Chrome" / "Application" / "chrome.exe",
pf86 / "Google" / "Chrome" / "Application" / "chrome.exe",
local / "Google" / "Chrome" / "Application" / "chrome.exe",
]
elif channel == "chromium":
candidates = [
pf / "Chromium" / "Application" / "chrome.exe",
local / "Chromium" / "Application" / "chrome.exe",
]
for path in candidates:
if path.is_file():
return path
return None
def mermaid_assets_dir() -> Path:
return diagrams_dir()
def mermaid_page_path() -> Path:
return mermaid_assets_dir() / MERMAID_PAGE_NAME
def mermaid_js_path() -> Path:
return mermaid_assets_dir() / MERMAID_JS_NAME
def user_data_root() -> Path:
if os.name == "nt":
return Path(os.environ.get("LOCALAPPDATA") or Path.home()) / "md2gost"
return Path.home() / ".md2gost"
def playwright_browsers_dir() -> Path:
env = os.environ.get("PLAYWRIGHT_BROWSERS_PATH")
if env and env not in ("0", "1"):
return Path(env)
return user_data_root() / "ms-playwright"
def ensure_playwright_browsers_env() -> Path:
"""Point Playwright at %LOCALAPPDATA%/md2gost/ms-playwright (or ~/.md2gost)."""
root = playwright_browsers_dir()
root.mkdir(parents=True, exist_ok=True)
os.environ["PLAYWRIGHT_BROWSERS_PATH"] = str(root)
return root
def mermaidx_available() -> bool:
try:
import mermaidx # noqa: F401
return True
except ImportError:
return False
def playwright_available() -> bool:
try:
from playwright.sync_api import sync_playwright # noqa: F401
return True
except ImportError:
return False
def _playwright_driver() -> tuple[str, str] | None:
try:
from playwright._impl._driver import compute_driver_executable
node, cli = compute_driver_executable()
return str(node), str(cli)
except Exception as exc:
_log.debug("Playwright driver: %s", exc)
return None
def chromium_installed() -> bool:
"""True if Playwright Chromium binary exists under PLAYWRIGHT_BROWSERS_PATH."""
if not playwright_available():
return False
ensure_playwright_browsers_env()
try:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
exe = Path(p.chromium.executable_path)
return exe.is_file()
except Exception:
return False
def detect_system_browser_channel(*, probe_launch: bool = False) -> str | None:
"""
Return first usable Playwright channel (msedge / chrome / chromium).
By default only checks known install paths (fast, for GUI status).
Set probe_launch=True to actually launch (used once before first render).
"""
global _cached_system_channel
if _cached_system_channel is not _UNSET and not probe_launch:
return _cached_system_channel # type: ignore[return-value]
if not playwright_available():
_cached_system_channel = None
return None
# Path probe first (Windows); on other OS fall through to launch probe.
if os.name == "nt":
for channel in _SYSTEM_CHANNELS:
if _windows_browser_exe(channel) is not None:
_cached_system_channel = channel
return channel
if not probe_launch:
_cached_system_channel = None
return None
if not probe_launch and os.name == "nt":
return None
try:
from playwright.sync_api import sync_playwright
except ImportError:
_cached_system_channel = None
return None
ensure_playwright_browsers_env()
for channel in _SYSTEM_CHANNELS:
try:
with sync_playwright() as p:
browser = p.chromium.launch(channel=channel, headless=True)
browser.close()
_cached_system_channel = channel
return channel
except Exception as exc:
_log.debug("channel %s unavailable: %s", channel, exc)
_cached_system_channel = None
return None
def install_playwright_chromium() -> bool:
"""Download Playwright Chromium into user cache. Safe for frozen exe."""
if not playwright_available():
_log.warning("playwright не установлен — нельзя скачать Chromium")
return False
driver = _playwright_driver()
if not driver:
_log.warning("Playwright driver не найден")
return False
node, cli = driver
root = ensure_playwright_browsers_env()
env = os.environ.copy()
env["PLAYWRIGHT_BROWSERS_PATH"] = str(root)
cmd = [node, cli, "install", "chromium"]
_log.info("Установка Playwright Chromium → %s", root)
try:
proc = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=600,
check=False,
env=env,
)
except (OSError, subprocess.TimeoutExpired) as exc:
_log.warning("install chromium failed: %s", exc)
return False
if proc.returncode != 0:
_log.warning(
"playwright install chromium → %s\n%s\n%s",
proc.returncode,
proc.stdout,
proc.stderr,
)
return False
ok = chromium_installed()
if not ok:
_log.warning("Chromium скачан, но executable_path не найден")
return ok
def mermaid_engine_status() -> str:
"""Human-readable Mermaid status for the GUI."""
lines: list[str] = []
if not playwright_available():
lines.append("Mermaid browser: playwright не установлен")
else:
ch = detect_system_browser_channel()
if ch:
lines.append(f"Mermaid browser: системный ({ch})")
else:
lines.append("Mermaid browser: системный Chrome/Edge не найден")
if chromium_installed():
lines.append(f"Mermaid Chromium: есть ({playwright_browsers_dir()})")
else:
lines.append(
"Mermaid Chromium: нет. Кнопка «Скачать headless Chromium» "
"или md2gost --install-chromium"
)
if mermaidx_available():
lines.append("Mermaid QuickJS (mermaidx): готов (офлайн без браузера)")
else:
lines.append("Mermaid QuickJS: mermaidx не установлен")
page = mermaid_page_path()
js = mermaid_js_path()
if page.is_file() and js.is_file():
lines.append(f"Mermaid assets: {js.name} + {page.name}")
else:
lines.append("Mermaid assets: нет mermaid.min.js / mermaid_page.html")
return "\n".join(lines)
def svg_to_png_bytes(svg: str, *, scale: float = 1.0) -> bytes | None:
try:
from mermaidx import svg_to_png
except ImportError:
return None
try:
if scale and scale > 1:
return _svg_to_png_scaled(svg, float(scale))
return svg_to_png(svg)
except Exception as exc:
_log.warning("svg→png failed: %s", exc)
return None
def _svg_to_png_scaled(svg: str, scale: float) -> bytes:
from mermaidx import svg_to_png
if scale <= 1:
return svg_to_png(svg)
# Parse width="N" or width="Npx" from root svg; multiply for sharper raster.
import re
m = re.search(r"<svg\b[^>]*\bwidth=['\"]([\d.]+)(px)?['\"]", svg, re.I)
if m:
w = float(m.group(1)) * scale
return svg_to_png(svg, width=w)
# Fallback: resvg default size, then Pillow upsample (last resort)
png = svg_to_png(svg)
try:
from io import BytesIO
from PIL import Image
im = Image.open(BytesIO(png))
im = im.resize(
(max(1, int(im.width * scale)), max(1, int(im.height * scale))),
Image.Resampling.LANCZOS,
)
buf = BytesIO()
im.save(buf, format="PNG")
return buf.getvalue()
except Exception:
return png
def _render_quickjs(source: str, *, scale: float = 1.0) -> tuple[str, bytes] | None:
if not mermaidx_available():
return None
try:
from mermaidx import render
except ImportError:
return None
try:
prepared = prepare_mermaid_source(source, html_labels=False)
diagram = render(prepared, backend="quickjs", config=_MERMAID_CONFIG_QUICKJS)
svg = diagram.svg()
if not isinstance(svg, str) or "<svg" not in svg.lower():
return None
scale_kw = {"scale": float(scale)} if scale and scale > 1 else {}
png = diagram.png(**scale_kw)
if not isinstance(png, (bytes, bytearray)) or not png.startswith(b"\x89PNG"):
png_b = svg_to_png_bytes(svg, scale=scale)
if not png_b:
return None
png = png_b
return svg, bytes(png)
except Exception as exc:
_log.warning("mermaidx QuickJS failed: %s", exc)
return None
class _BrowserSession:
"""One Playwright browser for the whole conversion (system channel or bundled Chromium)."""
def __init__(self, *, channel: str | None, use_bundled: bool) -> None:
from playwright.sync_api import sync_playwright
ensure_playwright_browsers_env()
self._pw_cm = sync_playwright()
self._pw = self._pw_cm.__enter__()
kwargs: dict = {"headless": True}
if channel:
kwargs["channel"] = channel
elif not use_bundled:
raise RuntimeError("no browser channel")
self._browser = self._pw.chromium.launch(**kwargs)
# device_scale_factor set per render via new context when needed; default 1.
self._context = self._browser.new_context(device_scale_factor=1)
self._page = self._context.new_page()
page_file = mermaid_page_path()
js_file = mermaid_js_path()
if not page_file.is_file() or not js_file.is_file():
raise FileNotFoundError(f"Mermaid assets missing under {mermaid_assets_dir()}")
self._page.goto(page_file.resolve().as_uri(), wait_until="load")
self._page.wait_for_function("typeof window.__md2gostRender === 'function'")
self._scale = 1.0
def _ensure_scale(self, scale: float) -> None:
"""Recreate context if DPR must change for sharper screenshots."""
want = max(1.0, float(scale) if scale else 1.0)
if abs(want - self._scale) < 0.01:
return
self._context.close()
self._context = self._browser.new_context(device_scale_factor=want)
self._page = self._context.new_page()
self._page.goto(mermaid_page_path().resolve().as_uri(), wait_until="load")
self._page.wait_for_function("typeof window.__md2gostRender === 'function'")
self._scale = want
def render_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]:
"""
Browser: htmlLabels + screenshot at viewBox size (layout + Cyrillic + hi-res).
"""
self._ensure_scale(scale)
prepared = prepare_mermaid_source(source, html_labels=True)
svg = self._page.evaluate(
"(code) => window.__md2gostRender(code)",
prepared,
)
if not isinstance(svg, str) or "<svg" not in svg.lower():
raise RuntimeError("browser mermaid.render returned no SVG")
# Mermaid sets width="100%" — force viewBox pixel size before capture.
dims = self._page.evaluate(
"""() => {
const el = document.querySelector('#out svg');
if (!el) return null;
let w = 0, h = 0;
try {
const vb = el.viewBox && el.viewBox.baseVal;
if (vb && vb.width > 0 && vb.height > 0) {
w = vb.width;
h = vb.height;
}
} catch (e) {}
if (!(w > 0 && h > 0)) {
try {
const b = el.getBBox();
w = b.width;
h = b.height;
} catch (e) {}
}
if (!(w > 0 && h > 0)) return null;
el.setAttribute('width', String(w));
el.setAttribute('height', String(h));
el.style.width = w + 'px';
el.style.height = h + 'px';
el.style.maxWidth = 'none';
el.style.maxHeight = 'none';
return { w: Math.ceil(w), h: Math.ceil(h) };
}"""
)
if isinstance(dims, dict):
vw = max(1280, int(dims.get("w") or 0) + 64)
vh = max(720, int(dims.get("h") or 0) + 64)
self._page.set_viewport_size({"width": vw, "height": vh})
# Prefer screenshot: FO/HTML labels render correctly in a real browser.
png: bytes | None = None
loc = self._page.locator("#out svg").first
loc.wait_for(state="visible", timeout=15000)
shot = loc.screenshot(type="png", omit_background=False)
if isinstance(shot, (bytes, bytearray)) and shot.startswith(b"\x89PNG"):
png = bytes(shot)
if png is None and "foreignObject" not in svg:
png = svg_to_png_bytes(svg, scale=scale)
if not png:
raise RuntimeError("no PNG from browser path")
return svg, png
def close(self) -> None:
try:
self._browser.close()
except Exception:
pass
try:
self._pw_cm.__exit__(None, None, None)
except Exception:
pass
def reset_mermaid_session() -> None:
global _session
with _lock:
if _session is not None:
try:
_session.close()
except Exception:
pass
_session = None
def _get_session(*, prefer_system: bool = True) -> _BrowserSession | None:
global _session
with _lock:
if _session is not None:
return _session
if not playwright_available():
return None
page = mermaid_page_path()
if not page.is_file() or not mermaid_js_path().is_file():
return None
# 1) system browser
if prefer_system:
ch = detect_system_browser_channel(probe_launch=True)
if ch:
try:
_session = _BrowserSession(channel=ch, use_bundled=False)
_log.info("Mermaid: системный браузер (%s)", ch)
return _session
except Exception as exc:
_log.warning("Mermaid system browser failed: %s", exc)
_session = None
# Retry other channels once
for alt in _SYSTEM_CHANNELS:
if alt == ch:
continue
try:
_session = _BrowserSession(channel=alt, use_bundled=False)
_log.info("Mermaid: системный браузер (%s)", alt)
return _session
except Exception:
_session = None
# 2) Playwright Chromium
if chromium_installed():
try:
_session = _BrowserSession(channel=None, use_bundled=True)
_log.info("Mermaid: Playwright Chromium")
return _session
except Exception as exc:
_log.warning("Mermaid Chromium failed: %s", exc)
_session = None
return None
def _render_browser(source: str, *, scale: float = 1.0) -> tuple[str, bytes] | None:
session = _get_session(prefer_system=True)
if session is None:
return None
try:
return session.render_png(source, scale=scale)
except Exception as exc:
_log.warning("Mermaid browser render failed: %s", exc)
reset_mermaid_session()
return None
BackendName = Literal["browser", "quickjs"]
def render_mermaid_local(
source: str,
*,
scale: float = 1.0,
) -> tuple[str, bytes, BackendName] | None:
"""
Try local Mermaid backends in order: system/Chromium browser → QuickJS.
Returns (svg, png_bytes, backend) or None.
"""
got = _render_browser(source, scale=scale)
if got:
return got[0], got[1], "browser"
got = _render_quickjs(source, scale=scale)
if got:
return got[0], got[1], "quickjs"
return None
def write_mermaid_outputs(
source: str,
out_png: Path,
out_svg: Path | None,
*,
scale: float = 1.0,
) -> bool:
"""Render locally and write PNG (+ optional SVG)."""
result = render_mermaid_local(source, scale=scale)
if not result:
return False
svg, png, backend = result
out_png.write_bytes(png)
if out_svg is not None:
out_svg.write_text(svg, encoding="utf-8")
_log.info("Mermaid local ok (%s) → %s", backend, out_png.name)
return True
__all__ = [
"CACHE_KEY_PREFIX",
"chromium_installed",
"detect_system_browser_channel",
"ensure_playwright_browsers_env",
"install_playwright_chromium",
"mermaid_engine_status",
"mermaidx_available",
"playwright_available",
"playwright_browsers_dir",
"prepare_mermaid_source",
"render_mermaid_local",
"reset_mermaid_session",
"write_mermaid_outputs",
]
+35
View File
@@ -156,3 +156,38 @@ def ensure_continuous_page_numbers(document) -> None:
# Keep continuous: remove start attribute if present # Keep continuous: remove start attribute if present
if child.get(qn("w:start")) is not None: if child.get(qn("w:start")) is not None:
del child.attrib[qn("w:start")] del child.attrib[qn("w:start")]
def set_section_page_start(section, start: int) -> None:
"""Set w:pgNumType/@w:start so PAGE field begins at ``start`` in this section."""
if start < 1:
return
sect_pr = section._sectPr
pg = sect_pr.find(qn("w:pgNumType"))
if pg is None:
sect_pr.append(create_element("w:pgNumType", {"w:start": str(int(start))}))
else:
pg.set(qn("w:start"), str(int(start)))
def apply_page_number_start(
document,
start: int | None,
*,
front_sections: int = 0,
) -> None:
"""
Apply page-number offset for the body.
``start`` is None / <1 — leave continuous numbering (strip explicit starts).
``start`` >= 1 — first section after front matter restarts PAGE at that number;
later body sections stay continuous from there.
"""
ensure_continuous_page_numbers(document)
if start is None or int(start) < 1:
return
sections = list(document.sections)
idx = max(0, int(front_sections))
if idx >= len(sections):
return
set_section_page_start(sections[idx], int(start))
+23
View File
@@ -56,6 +56,8 @@ class ConvertRequest:
open_when_done: bool = False open_when_done: bool = False
check_pages: bool = False check_pages: bool = False
table_repeat_header: bool = False table_repeat_header: bool = False
# PAGE field start on body section; None = continuous from document start.
page_number_start: int | None = None
@dataclass @dataclass
@@ -121,6 +123,20 @@ def _fix_front_matter_after_compose(document, *, had_title: bool, had_assignment
ensure_continuous_page_numbers(document) ensure_continuous_page_numbers(document)
def _apply_page_offset(document, req: ConvertRequest, *, had_title: bool, had_assignment: bool) -> None:
from .page_geometry import apply_page_number_start
start = getattr(req, "page_number_start", None)
if start is None:
return
try:
start_i = int(start)
except (TypeError, ValueError):
return
front = int(bool(had_title)) + int(bool(had_assignment))
apply_page_number_start(document, start_i, front_sections=front)
def convert(req: ConvertRequest, log: LogFn | None = None) -> ConvertResult: def convert(req: ConvertRequest, log: LogFn | None = None) -> ConvertResult:
"""Run check and/or conversion. Does not re-raise; errors go into ConvertResult.""" """Run check and/or conversion. Does not re-raise; errors go into ConvertResult."""
emit: LogFn = log if log is not None else print emit: LogFn = log if log is not None else print
@@ -255,6 +271,13 @@ def convert(req: ConvertRequest, log: LogFn | None = None) -> ConvertResult:
had_assignment=bool(req.assignment), had_assignment=bool(req.assignment),
) )
_apply_page_offset(
document,
req,
had_title=bool(req.title),
had_assignment=bool(req.assignment),
)
document.core_properties.author = getuser() document.core_properties.author = getuser()
document.core_properties.comments = "Создано при помощи md2gost (ТЗ МИРЭА)" document.core_properties.comments = "Создано при помощи md2gost (ТЗ МИРЭА)"
try: try:
+109 -2
View File
@@ -1,10 +1,9 @@
from copy import copy from copy import copy
from typing import Generator, Any from typing import Generator
from docx.shared import Length, Parented, RGBColor from docx.shared import Length, Parented, RGBColor
from docx.text.paragraph import Paragraph as DocxParagraph from docx.text.paragraph import Paragraph as DocxParagraph
from docx.text.paragraph import Run as DocxRun from docx.text.paragraph import Run as DocxRun
from docx.enum.text import WD_LINE_SPACING
from docx.opc.constants import RELATIONSHIP_TYPE from docx.opc.constants import RELATIONSHIP_TYPE
from . import Renderable from . import Renderable
@@ -17,6 +16,14 @@ from ..util import create_element
from ..rendered_info import RenderedInfo from ..rendered_info import RenderedInfo
from ..latex_math import latex_to_omml, inline_omml from ..latex_math import latex_to_omml, inline_omml
_bookmark_seq = 0
def _next_bookmark_id() -> int:
global _bookmark_seq
_bookmark_seq += 1
return _bookmark_seq
class Link: class Link:
def __init__(self, url, docx_paragraph: DocxParagraph): def __init__(self, url, docx_paragraph: DocxParagraph):
@@ -48,6 +55,39 @@ class Link:
return self._hyperlink return self._hyperlink
class InternalLink:
"""Hyperlink to a bookmark in the same document (w:anchor)."""
def __init__(self, anchor: str, docx_paragraph: DocxParagraph):
self._docx_paragraph = docx_paragraph
self._hyperlink = create_element("w:hyperlink", {"w:anchor": anchor})
def add_run(
self,
text: str,
is_bold: bool = None,
is_italic: bool = None,
color: RGBColor = None,
strike_through: bool = None,
):
docx_run = DocxRun(create_element("w:r"), self._docx_paragraph)
self._hyperlink.append(docx_run._element)
docx_run.text = text
try:
docx_run.style = "Hyperlink"
except KeyError:
pass
docx_run.bold = is_bold
docx_run.italic = is_italic
if color is not None:
docx_run.font.color.rgb = color
docx_run.font.strike = strike_through
@property
def element(self):
return self._hyperlink
class Paragraph(Renderable): class Paragraph(Renderable):
def __init__(self, parent: Parented): def __init__(self, parent: Parented):
self._parent = parent self._parent = parent
@@ -70,6 +110,73 @@ class Paragraph(Renderable):
self._docx_paragraph.add_run()._element.\ self._docx_paragraph.add_run()._element.\
append(create_element("w:noBreakHyphen")) append(create_element("w:noBreakHyphen"))
def add_run_with_citations(
self,
text: str,
is_bold: bool = None,
is_italic: bool = None,
color: RGBColor = None,
strike_through: bool = None,
):
"""Split body text so [1] / [2, 3] become internal links to bibliography bookmarks."""
import re
from ..bibliography import CITE_RE
kw = dict(
is_bold=is_bold, is_italic=is_italic, color=color, strike_through=strike_through,
)
pos = 0
for m in CITE_RE.finditer(text):
if m.start() > pos:
self.add_run(text[pos:m.start()], **kw)
full = m.group(0)
inner = m.group(1)
self.add_run("[", **kw)
for token in re.split(r"(\s*,\s*)", inner):
if re.fullmatch(r"\d+(?:\.\d+)?", (token or "").strip()):
self.add_biblio_link(token.strip(), token, **kw)
elif token:
self.add_run(token, **kw)
suffix = full[1 + len(inner):-1]
if suffix:
self.add_run(suffix, **kw)
self.add_run("]", **kw)
pos = m.end()
if pos < len(text):
self.add_run(text[pos:], **kw)
def add_biblio_link(
self,
key: str,
text: str | None = None,
is_bold: bool = None,
is_italic: bool = None,
color: RGBColor = None,
strike_through: bool = None,
):
from ..bibliography import biblio_bookmark_name
link = InternalLink(biblio_bookmark_name(key), self._docx_paragraph)
link.add_run(
text if text is not None else key,
is_bold=is_bold,
is_italic=is_italic,
color=color,
strike_through=strike_through,
)
self._docx_paragraph._p.append(link.element)
return link
def wrap_with_bookmark(self, name: str) -> None:
"""Surround current paragraph content with a Word bookmark."""
bid = str(_next_bookmark_id())
p = self._docx_paragraph._p
start = create_element("w:bookmarkStart", {"w:id": bid, "w:name": name})
end = create_element("w:bookmarkEnd", {"w:id": bid})
p.insert(0, start)
p.append(end)
def add_reference(self, type_: str, name: str): def add_reference(self, type_: str, name: str):
"""Placeholder run resolved later via resolve_pending_refs().""" """Placeholder run resolved later via resolve_pending_refs()."""
run = self._docx_paragraph.add_run(f"{type_}?") run = self._docx_paragraph.add_run(f"{type_}?")
+15 -2
View File
@@ -37,10 +37,23 @@ class RenderableFactory:
classes = [] classes = []
for child in children: for child in children:
if isinstance(child, (extended_markdown.RawText, extended_markdown.Literal)): if isinstance(child, (extended_markdown.RawText, extended_markdown.Literal)):
paragraph_or_link.add_run(child.children, text = child.children
if isinstance(paragraph_or_link, Paragraph) and hasattr(
paragraph_or_link, "add_run_with_citations"
):
paragraph_or_link.add_run_with_citations(
text,
is_bold=extended_markdown.StrongEmphasis in classes or None, is_bold=extended_markdown.StrongEmphasis in classes or None,
is_italic=extended_markdown.Emphasis in classes or None, is_italic=extended_markdown.Emphasis in classes or None,
strike_through=extended_markdown.Strikethrough in classes or None) strike_through=extended_markdown.Strikethrough in classes or None,
)
else:
paragraph_or_link.add_run(
text,
is_bold=extended_markdown.StrongEmphasis in classes or None,
is_italic=extended_markdown.Emphasis in classes or None,
strike_through=extended_markdown.Strikethrough in classes or None,
)
elif isinstance(child, extended_markdown.CodeSpan): elif isinstance(child, extended_markdown.CodeSpan):
paragraph_or_link.add_run(child.children, is_italic=True) paragraph_or_link.add_run(child.children, is_italic=True)
elif isinstance(child, extended_markdown.Image): elif isinstance(child, extended_markdown.Image):
+4
View File
@@ -14,6 +14,8 @@ dependencies = [
"latex2mathml>=3.76.0,<4", "latex2mathml>=3.76.0,<4",
"pygments>=2.16.1,<3", "pygments>=2.16.1,<3",
"lxml>=4.9,<6", "lxml>=4.9,<6",
"mermaidx>=0.9.0,<1",
"playwright>=1.40.0,<2",
] ]
[project.scripts] [project.scripts]
@@ -75,6 +77,8 @@ matplotlib = { version = "3.7.2", platform = "!= linux" }
requests = "^2.31.0" requests = "^2.31.0"
latex2mathml = "^3.76.0" latex2mathml = "^3.76.0"
pygments = "^2.16.1" pygments = "^2.16.1"
mermaidx = "^0.9.0"
playwright = "^1.40.0"
[tool.poetry.scripts] [tool.poetry.scripts]
md2gost = "md2gost.__main__:main" md2gost = "md2gost.__main__:main"
+33
View File
@@ -0,0 +1,33 @@
babel==2.18.0
certifi==2026.7.22
charset-normalizer==3.5.1
colorama==0.4.6
docxcompose==1.4.1
exceptiongroup==1.3.1
freetype-py==2.5.1
greenlet==3.5.5
idna==3.19
iniconfig==2.3.0
latex2mathml==3.81.0
lxml==5.4.0
marko==2.2.4
mermaidx==0.9.5
packaging==26.3
pillow==10.4.0
pip==25.0.1
playwright==1.62.0
pluggy==1.6.0
pyee==13.0.1
pygments==2.21.0
pytest==9.1.1
python-docx==0.8.11
quickjs-ng==0.16.2.1
requests==2.34.2
resvg-py==0.5.0
setuptools==78.0.1
six==1.17.0
termaid==0.8.0
tomli==2.4.1
typing-extensions==4.16.0
urllib3==2.7.0
wheel==0.45.1
+34
View File
@@ -0,0 +1,34 @@
"""Download pinned mermaid.min.js into md2gost/diagrams (for offline browser render)."""
from __future__ import annotations
import sys
from pathlib import Path
import requests
ROOT = Path(__file__).resolve().parents[1]
DEST = ROOT / "md2gost" / "diagrams" / "mermaid.min.js"
MERMAID_VERSION = "11.4.1"
URL = f"https://cdn.jsdelivr.net/npm/mermaid@{MERMAID_VERSION}/dist/mermaid.min.js"
def main() -> int:
DEST.parent.mkdir(parents=True, exist_ok=True)
print(f"Скачиваю Mermaid {MERMAID_VERSION}{DEST}")
try:
resp = requests.get(URL, timeout=120)
resp.raise_for_status()
if len(resp.content) < 100_000:
print("Слишком маленький ответ — не похоже на mermaid.min.js", file=sys.stderr)
return 1
DEST.write_bytes(resp.content)
except Exception as exc:
print(f"Ошибка: {exc}", file=sys.stderr)
return 1
print(f"OK ({DEST.stat().st_size} bytes)")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+46
View File
@@ -0,0 +1,46 @@
"""Bibliography citation hyperlinks."""
from docx import Document
from docx.oxml.ns import qn
from md2gost.bibliography import biblio_bookmark_name
from md2gost.bibliography_renderable import Bibliography
from md2gost.bibliography import BiblioEntry
from md2gost.renderable.paragraph import Paragraph
def test_biblio_bookmark_name():
assert biblio_bookmark_name("1") == "biblio_1"
assert biblio_bookmark_name("1.5") == "biblio_1_5"
def test_entry_has_bookmark():
doc = Document()
e = BiblioEntry(key="1", text="Иванов И. И. Книга.")
p = Bibliography._make_entry(doc._body, e)
xml = p._docx_paragraph._p.xml
assert 'w:name="biblio_1"' in xml
assert "bookmarkStart" in xml
assert "bookmarkEnd" in xml
def test_cite_becomes_internal_hyperlink():
doc = Document()
p = Paragraph(doc._body)
p.add_run_with_citations("See [1] and [2, 3].")
xml = p._docx_paragraph._p.xml
assert 'w:anchor="biblio_1"' in xml
assert 'w:anchor="biblio_2"' in xml
assert 'w:anchor="biblio_3"' in xml
assert ">1</w:t>" in xml
assert ">2</w:t>" in xml
assert ">3</w:t>" in xml
def test_plain_text_without_cites_unchanged():
doc = Document()
p = Paragraph(doc._body)
p.add_run_with_citations("Просто текст без ссылок.")
xml = p._docx_paragraph._p.xml
assert "w:hyperlink" not in xml
assert "Просто текст без ссылок." in p._docx_paragraph.text
+4
View File
@@ -356,6 +356,9 @@ def test_render_mermaid_skips_jar(tmp_path):
calls["jar"] += 1 calls["jar"] += 1
return False return False
def fake_local(*args, **kwargs):
return False
def fake_kroki(source, diagram_type, base_url, out_path, fmt="png"): def fake_kroki(source, diagram_type, base_url, out_path, fmt="png"):
calls["kroki"].append((diagram_type, fmt, base_url)) calls["kroki"].append((diagram_type, fmt, base_url))
if fmt == "png": if fmt == "png":
@@ -366,6 +369,7 @@ def test_render_mermaid_skips_jar(tmp_path):
configure_diagrams(fallback="remote", cache_dir=str(tmp_path)) configure_diagrams(fallback="remote", cache_dir=str(tmp_path))
with patch("md2gost.diagram_renderer._render_plantuml_jar", side_effect=fake_jar), \ with patch("md2gost.diagram_renderer._render_plantuml_jar", side_effect=fake_jar), \
patch("md2gost.mermaid_renderer.write_mermaid_outputs", side_effect=fake_local), \
patch("md2gost.diagram_renderer._render_kroki", side_effect=fake_kroki): patch("md2gost.diagram_renderer._render_kroki", side_effect=fake_kroki):
result = render_diagram("mermaid", "flowchart LR\nA-->B", cache_dir=str(tmp_path)) result = render_diagram("mermaid", "flowchart LR\nA-->B", cache_dir=str(tmp_path))
assert calls["jar"] == 0 assert calls["jar"] == 0
+153
View File
@@ -0,0 +1,153 @@
"""Tests for local Mermaid rendering chain."""
from __future__ import annotations
from pathlib import Path
from unittest.mock import patch
import pytest
from md2gost.diagram_renderer import configure_diagrams, render_diagram
from md2gost.mermaid_renderer import (
CACHE_KEY_PREFIX,
render_mermaid_local,
write_mermaid_outputs,
)
def test_cache_key_prefix_stable():
assert CACHE_KEY_PREFIX.startswith("mmd-local")
def test_prepare_mermaid_source_injects_init():
from md2gost.mermaid_renderer import prepare_mermaid_source
out = prepare_mermaid_source("flowchart LR\nA-->B", html_labels=True)
assert out.startswith("%%{init")
assert "htmlLabels': true" in out
assert "flowchart LR" in out
assert prepare_mermaid_source(out) == out
qj = prepare_mermaid_source("flowchart LR\nA-->B", html_labels=False)
assert "htmlLabels': false" in qj
def test_quickjs_cyrillic_no_foreign_object():
"""QuickJS uses htmlLabels:false → SVG text; resvg can paint Cyrillic."""
from md2gost.mermaid_renderer import _render_quickjs
got = _render_quickjs('flowchart LR\nA["Акционеры"] --> B["Тест"]', scale=1)
assert got is not None
svg, png = got
assert "Акционеры" in svg
assert "foreignObject" not in svg
assert "<text" in svg
assert png.startswith(b"\x89PNG")
assert len(png) > 2000
def test_write_mermaid_outputs_quickjs(tmp_path, monkeypatch):
png = b"\x89PNG\r\n\x1a\n" + b"\x00" * 40
svg = '<svg xmlns="http://www.w3.org/2000/svg"><rect/></svg>'
def fake_local(source, *, scale=1.0):
assert "flowchart" in source
return svg, png, "quickjs"
monkeypatch.setattr(
"md2gost.mermaid_renderer.render_mermaid_local", fake_local,
)
out_png = tmp_path / "a.png"
out_svg = tmp_path / "a.svg"
assert write_mermaid_outputs("flowchart LR\nA-->B", out_png, out_svg, scale=2)
assert out_png.read_bytes().startswith(b"\x89PNG")
assert "<svg" in out_svg.read_text(encoding="utf-8")
def test_render_mermaid_local_order_browser_then_quickjs(monkeypatch):
calls = []
def fake_browser(source, *, scale=1.0):
calls.append("browser")
return None
def fake_qj(source, *, scale=1.0):
calls.append("quickjs")
return (
'<svg xmlns="http://www.w3.org/2000/svg"/>',
b"\x89PNG\r\n\x1a\n" + b"\x00" * 20,
)
monkeypatch.setattr("md2gost.mermaid_renderer._render_browser", fake_browser)
monkeypatch.setattr("md2gost.mermaid_renderer._render_quickjs", fake_qj)
got = render_mermaid_local("flowchart LR\nA-->B")
assert got is not None
assert got[2] == "quickjs"
assert calls == ["browser", "quickjs"]
def test_render_diagram_uses_local_before_kroki(tmp_path):
png = b"\x89PNG\r\n\x1a\n" + b"\x00" * 20
svg = b'<svg xmlns="http://www.w3.org/2000/svg"/>'
kroki_calls = []
def fake_write(source, out_png, out_svg, *, scale=1.0):
Path(out_png).write_bytes(png)
if out_svg is not None:
Path(out_svg).write_bytes(svg)
return True
def fake_kroki(source, diagram_type, base_url, out_path, fmt="png"):
kroki_calls.append((diagram_type, fmt, base_url))
return False
configure_diagrams(fallback="remote", cache_dir=str(tmp_path), diagram_scale=2)
with patch("md2gost.diagram_renderer._render_plantuml_jar", return_value=False), \
patch("md2gost.mermaid_renderer.write_mermaid_outputs", side_effect=fake_write), \
patch("md2gost.diagram_renderer._render_kroki", side_effect=fake_kroki):
result = render_diagram(
"mermaid", "flowchart LR\nA-->B", cache_dir=str(tmp_path),
)
assert kroki_calls == []
assert Path(result.png_path).read_bytes().startswith(b"\x89PNG")
assert result.pixel_scale == 2.0
def test_render_diagram_falls_to_kroki_if_local_fails(tmp_path):
png = b"\x89PNG\r\n\x1a\n" + b"\x00" * 20
kroki_calls = []
def fake_write(*args, **kwargs):
return False
def fake_kroki(source, diagram_type, base_url, out_path, fmt="png"):
kroki_calls.append((diagram_type, fmt, base_url))
if fmt == "png":
out_path.write_bytes(png)
else:
out_path.write_bytes(b"<svg xmlns='http://www.w3.org/2000/svg'/>")
return True
configure_diagrams(fallback="remote", cache_dir=str(tmp_path))
with patch("md2gost.diagram_renderer._render_plantuml_jar", return_value=False), \
patch("md2gost.mermaid_renderer.write_mermaid_outputs", side_effect=fake_write), \
patch("md2gost.diagram_renderer._render_kroki", side_effect=fake_kroki):
result = render_diagram("mermaid", "flowchart LR\nA-->B", cache_dir=str(tmp_path))
assert any(t == "mermaid" and f == "png" for t, f, _ in kroki_calls)
assert Path(result.png_path).read_bytes().startswith(b"\x89PNG")
assert result.pixel_scale == 1.0
@pytest.mark.skipif(
__import__("importlib").util.find_spec("mermaidx") is None,
reason="mermaidx not installed",
)
def test_quickjs_real_render():
from md2gost.mermaid_renderer import _render_quickjs
got = _render_quickjs("flowchart LR\nA-->B", scale=1)
assert got is not None
svg, png = got
assert "<svg" in svg.lower()
assert png.startswith(b"\x89PNG")
+35
View File
@@ -0,0 +1,35 @@
"""Page number start / offset helpers."""
from docx import Document
from docx.oxml.ns import qn
from md2gost.page_geometry import apply_page_number_start, set_section_page_start
def test_set_section_page_start():
doc = Document()
section = doc.sections[0]
set_section_page_start(section, 3)
pg = section._sectPr.find(qn("w:pgNumType"))
assert pg is not None
assert pg.get(qn("w:start")) == "3"
def test_apply_page_number_start_skips_front():
doc = Document()
doc.add_section() # front
doc.add_section() # body
apply_page_number_start(doc, 2, front_sections=1)
front = doc.sections[0]._sectPr.find(qn("w:pgNumType"))
body = doc.sections[1]._sectPr.find(qn("w:pgNumType"))
assert front is None or front.get(qn("w:start")) is None
assert body is not None
assert body.get(qn("w:start")) == "2"
def test_apply_page_number_start_none_strips():
doc = Document()
set_section_page_start(doc.sections[0], 5)
apply_page_number_start(doc, None)
pg = doc.sections[0]._sectPr.find(qn("w:pgNumType"))
assert pg is None or pg.get(qn("w:start")) is None
Generated
+267
View File
@@ -233,6 +233,92 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/93/6e/bd7fbfacca077bc6f34f1a1109800a2c41ab50f4704d3a0507ba41009915/freetype_py-2.5.1-py3-none-win_amd64.whl", hash = "sha256:0b7f8e0342779f65ca13ef8bc103938366fecade23e6bb37cb671c2b8ad7f124", size = 814608, upload-time = "2024-08-29T18:32:24.648Z" }, { url = "https://files.pythonhosted.org/packages/93/6e/bd7fbfacca077bc6f34f1a1109800a2c41ab50f4704d3a0507ba41009915/freetype_py-2.5.1-py3-none-win_amd64.whl", hash = "sha256:0b7f8e0342779f65ca13ef8bc103938366fecade23e6bb37cb671c2b8ad7f124", size = 814608, upload-time = "2024-08-29T18:32:24.648Z" },
] ]
[[package]]
name = "greenlet"
version = "3.5.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0b/d8/7cc97c142388aef03f622e001c572c4f84e9252a439549d483f555771970/greenlet-3.5.5.tar.gz", hash = "sha256:adb4bae02e91a8e863e48b177e4014bdcac8a6b5e047ea1df687a61534b85e6c", size = 207585, upload-time = "2026-08-10T15:09:36.136Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/07/67/07ecd6d85c0f253363cbc4ac9e7dd048ca571a267fbccfea084d6009ac3b/greenlet-3.5.5-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:816230f469381ad0a43abc9fa8dda5a699e32fb78958dde32ded93213b70a667", size = 292971, upload-time = "2026-08-10T13:28:09.837Z" },
{ url = "https://files.pythonhosted.org/packages/24/35/426733bc24247ee17bb76df90f550c299ff5f8572b2bdd7cacb5c53fd994/greenlet-3.5.5-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5433cf291e0ef9114bd14d0d824db6e5e4a43033234bca48181a9597acca07b", size = 609290, upload-time = "2026-08-10T14:14:32.273Z" },
{ url = "https://files.pythonhosted.org/packages/9c/dc/17d3a5acceb2fd0bbc9682a228117b719cdfb68085e6dbb33fa325755f27/greenlet-3.5.5-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:19d59f068887d8c5907fc177f27683413ace3011b6ed646c0b309266e74a6502", size = 622650, upload-time = "2026-08-10T14:27:22.004Z" },
{ url = "https://files.pythonhosted.org/packages/74/3f/b31e6bd6adb8f80492efb6a47e8f9cd0e7335db5aac2795b1876d5afcfee/greenlet-3.5.5-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:86c5113d698cb8d927b2750bb1f1d59eefe3a37e0e0217491aee29a7f84ef52c", size = 629560, upload-time = "2026-08-10T14:30:04.733Z" },
{ url = "https://files.pythonhosted.org/packages/51/91/00b3c0566316c6f383ea16ee05388ec4f57f6e0afa49e72ae471eda8c47f/greenlet-3.5.5-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff00e12102358292087274dfb1669132387ff6e7920ebf9d85f4826ce0d3a56", size = 622819, upload-time = "2026-08-10T13:40:46.562Z" },
{ url = "https://files.pythonhosted.org/packages/18/5e/58c561efde575c4ca070030e2e0513e8d1b07b76d8a2d84a9bcef1becd42/greenlet-3.5.5-cp310-cp310-manylinux_2_39_riscv64.whl", hash = "sha256:c69bed34470abfcd456984fdadaa18e62169af4480335c45f3c32d1d9c12e638", size = 425489, upload-time = "2026-08-10T14:29:59.768Z" },
{ url = "https://files.pythonhosted.org/packages/3c/78/52de4f7ac9152ad1dbc8f437895c1e573d30ee1e1427e0f91a280e2417b6/greenlet-3.5.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:523bb8e27614d77101ea7a8cf59f8d91219b72d5c29f6a038c92b50828bfa8d0", size = 1582164, upload-time = "2026-08-10T14:15:02.645Z" },
{ url = "https://files.pythonhosted.org/packages/f6/ff/c3855a00c2417e8f61c7b9f0bc4f8f599c6928f5c15681ad251e78a91e05/greenlet-3.5.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f1e2db190db51c17433eee424803818cf0670bf049d9cfe0dd07be111d1aa7c4", size = 1648807, upload-time = "2026-08-10T13:40:27.471Z" },
{ url = "https://files.pythonhosted.org/packages/70/21/d29ff6a79dbd1ec1fe74c155d25d4c5a85e221d6b9ffc2cc7a709e7c8c39/greenlet-3.5.5-cp310-cp310-win_amd64.whl", hash = "sha256:740e544169527b82695ce76af2f7ad6f030904658f2f3921a1d245771fb88cfc", size = 322832, upload-time = "2026-08-10T13:28:10.85Z" },
{ url = "https://files.pythonhosted.org/packages/4e/a3/07297917485ee2ca85bc3c8dc6ed85ad3fffcf424047fba62671dba68e97/greenlet-3.5.5-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:be63afcbbccfad3dd95a1ba12ada84dab2ef32031973d80b5b92df67fa763a61", size = 294165, upload-time = "2026-08-10T13:25:17.987Z" },
{ url = "https://files.pythonhosted.org/packages/db/51/6f732f9314cda54c5fd48a7620c7160f4f286967e8045ad94b9d66ce80b7/greenlet-3.5.5-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a268024ce2d7d2b04694bf1594058981a9fa663d1df4b762dee499211ed7c1c", size = 613610, upload-time = "2026-08-10T14:14:33.829Z" },
{ url = "https://files.pythonhosted.org/packages/d8/c0/b27589e25d220289edcd4d582b2b17b83058d1a56d53d971b6ea1a34f10d/greenlet-3.5.5-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35cbb8bf55ace57fbccb4fb8622c4521713acd8691e77f4696d416ea7ca527da", size = 625481, upload-time = "2026-08-10T14:27:23.647Z" },
{ url = "https://files.pythonhosted.org/packages/39/82/5c873dbb4fb001d22fbbd50e80d4c1b0181ddae106856132160f84b94e88/greenlet-3.5.5-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:abc8bc8d9f935cd685457545b6a53863a877fdc12c2c0f5ee9beee18d9db139c", size = 633329, upload-time = "2026-08-10T14:30:06.062Z" },
{ url = "https://files.pythonhosted.org/packages/51/2d/f2c928218ac52f26d7a2c188c171d1b7e728b23782cb3347e7b4fce1493a/greenlet-3.5.5-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cc6df89ec5302337adc9cf096221cbed2510fd444b0e0f1586cf0470740864", size = 624562, upload-time = "2026-08-10T13:40:48.064Z" },
{ url = "https://files.pythonhosted.org/packages/70/12/f7df98e72a8eb4a7edfec5a08d6d1a4ab53a52c95ba0b2ea6c10b8dd9bd0/greenlet-3.5.5-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:3134291427bb0f3526e9d90311988caf336eb43730e95244997a4fb15f45144f", size = 428145, upload-time = "2026-08-10T14:30:01.071Z" },
{ url = "https://files.pythonhosted.org/packages/3e/4a/92fc51d5d35912f4f06eec037ba347985defd0be47463a010a325634d9d2/greenlet-3.5.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d9b454c5fc48aeaa7c4337813dbf513a6870468e426438a04d922c6d0fe63db", size = 1584909, upload-time = "2026-08-10T14:15:04.343Z" },
{ url = "https://files.pythonhosted.org/packages/ac/58/ed98b80ac5738c149a5258544843c45601ade1fd70f61740cdaead6351b3/greenlet-3.5.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03551ed792cb1b4fc0277a0c60dfd8c343894a0ba06fe60dcd22f568b433da39", size = 1651184, upload-time = "2026-08-10T13:40:28.879Z" },
{ url = "https://files.pythonhosted.org/packages/d8/be/b582ceb80cefdf9d8da34078714e4b12b3d16f509dee0f65e40a5cc8fc7d/greenlet-3.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:ab3df3dffb58bf70564e93a5cec7941e4d9faa5a36cc4234a10d3131afe04f53", size = 323280, upload-time = "2026-08-10T13:26:07.495Z" },
{ url = "https://files.pythonhosted.org/packages/4d/18/5313c4c58598c38b0373c013e4ff2b3e6d258aaaa338f373335ebecdaddd/greenlet-3.5.5-cp311-cp311-win_arm64.whl", hash = "sha256:2b70a766135540c472ac1393d57c2e1b4a2eb85bf526a1e41e6d096173a8cee5", size = 307785, upload-time = "2026-08-10T13:28:34.874Z" },
{ url = "https://files.pythonhosted.org/packages/2e/7e/9ecd0285e3153532ae07aeb88063c43c72b4221cf0d4d123b02f3682e3ff/greenlet-3.5.5-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:49520f0c95a48b42cf55414b8e8479beb274ea70431afc33e3f79903c71f4380", size = 295809, upload-time = "2026-08-10T13:25:34.023Z" },
{ url = "https://files.pythonhosted.org/packages/35/73/60e4bbcc89252037b18087f2ec16405d5b2d5be42dde191bbf3667e96102/greenlet-3.5.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55272212cbc5f43d1d723725ab931f1939969b7e9523882ca58b55061769d053", size = 611910, upload-time = "2026-08-10T14:14:35.18Z" },
{ url = "https://files.pythonhosted.org/packages/a4/17/cd5134be659cd4a443e7a61ae670dabec165a814c51162916d637b6dd38e/greenlet-3.5.5-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655bca754a2ef4efcb0eb48a94d3f4593536d0f3d48f8ed44343c01d16a92f95", size = 624198, upload-time = "2026-08-10T14:27:25.229Z" },
{ url = "https://files.pythonhosted.org/packages/9b/30/87c212b5c684d0e72974f1063b7a9687631e8985902c06e1016542c874e7/greenlet-3.5.5-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ca5d6ae0739e5764f2cfcfaa562ac5a990cbdaedca93251c5e3cf07c362371f", size = 629504, upload-time = "2026-08-10T14:30:07.967Z" },
{ url = "https://files.pythonhosted.org/packages/78/ac/5c5b959999b6f09c3026b5dfe171575bc3121c5236ce74f495096f25b203/greenlet-3.5.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:147b25a42e5ca5be3d42356e8f608b37af715a1c196e9bf9d1627f3341adfe1d", size = 621439, upload-time = "2026-08-10T13:40:49.391Z" },
{ url = "https://files.pythonhosted.org/packages/63/2c/eb487fafc9f50ffff2b1e0b697f70fb34bf150821c08ab225aacf5583a7e/greenlet-3.5.5-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:1b5ed9162c0c098e0bbc2cf88a94f433c1b8926f831745252e099e5d83e17759", size = 432462, upload-time = "2026-08-10T14:30:02.309Z" },
{ url = "https://files.pythonhosted.org/packages/c8/8b/6acf112ed8aee499f25b4d6949820fb02ac950ff9c1f3d793bd5be0599f2/greenlet-3.5.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:27493374cff1d1b7919dc8126547f2aea582737e3046147b434b1e12de56389b", size = 1581342, upload-time = "2026-08-10T14:15:05.653Z" },
{ url = "https://files.pythonhosted.org/packages/b8/d7/734e5f198888876b42d7616ff6644c075baf6b8a2412deadd6b0e1b8b20c/greenlet-3.5.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:12e2ee66c2aba86133f10fd99d6a8856c6d351ffb7be0e4d52ef2cc5fbb705b2", size = 1645744, upload-time = "2026-08-10T13:40:30.353Z" },
{ url = "https://files.pythonhosted.org/packages/de/30/1f42b88dc587b5899ee50616ad56ee40cafaf225df4fb829f10183c62a5c/greenlet-3.5.5-cp312-cp312-win_amd64.whl", hash = "sha256:49ddacd36af37735fab103846f4ee4d18a492dde72730d1699c0c8ebe30d9f18", size = 324171, upload-time = "2026-08-10T13:28:44.472Z" },
{ url = "https://files.pythonhosted.org/packages/76/e5/4dee4d8d2e603fe5fdd7b444e63219f7b9bd852c60c6214511c7157cbe88/greenlet-3.5.5-cp312-cp312-win_arm64.whl", hash = "sha256:5f1b1ff4828cdc1aba4266aff814085d04a1d07959287219af021b838b265d52", size = 308362, upload-time = "2026-08-10T13:26:46.839Z" },
{ url = "https://files.pythonhosted.org/packages/fb/3d/8cef5f724ec0d4add2af8961d504535ec60c3cca9e464f6d03bdba29d85b/greenlet-3.5.5-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:b79fd2a5bc099b5e744f34c4c9a58954a5f4cb7529fb4b6e8446057d61b6edaa", size = 294730, upload-time = "2026-08-10T13:27:51.206Z" },
{ url = "https://files.pythonhosted.org/packages/88/4b/8e7aa3f514273aecff30a16ab1bac09ff54cfc7e6860fdd8058c37ff2499/greenlet-3.5.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:634cf15a233a949136879dd388e25d3296e16f3f1e217d2456797b8579ebc6ed", size = 614536, upload-time = "2026-08-10T14:14:36.589Z" },
{ url = "https://files.pythonhosted.org/packages/85/48/4e95e9dd5a8a397dc6a6345dd7f1935113d0fca4f85e89d3976da9cd988d/greenlet-3.5.5-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:499adea519f748407fc6806d20eedabac2884fd73b9f38d81236e190ba20dfef", size = 626924, upload-time = "2026-08-10T14:27:27.048Z" },
{ url = "https://files.pythonhosted.org/packages/0e/84/eaa476d6bf3816828d0d70e80dcc36bf30a058233bd889e707e693f6e860/greenlet-3.5.5-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7278591501941bb2456af102bb9cd59aab48c6cfd6e2dd68fa1290bb0c49a42", size = 632726, upload-time = "2026-08-10T14:30:09.874Z" },
{ url = "https://files.pythonhosted.org/packages/89/5d/398a1c71fa7a277deeb376c999979de6786f08fc2d5747a0b9d6e11738dd/greenlet-3.5.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2eabb980975cba5b93a95f6f69287d05fc05ac955bfd6a320a7c083eeb52c0b0", size = 623906, upload-time = "2026-08-10T13:40:50.501Z" },
{ url = "https://files.pythonhosted.org/packages/d0/f2/0cc2849ede68579291e9c59b3ab6ec1958f98681cca5b14d8fc75bf674a4/greenlet-3.5.5-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:4dfc7c4470354e7b09184d1a3a985761053a2fd694ddb5b5c80242afc2c8c90b", size = 434966, upload-time = "2026-08-10T14:30:03.729Z" },
{ url = "https://files.pythonhosted.org/packages/04/1b/745450fc5ea9e0cb17d840d248f284db3363de736d362c7d2d883e3eadba/greenlet-3.5.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:03115c2e0a371999bf8ae616aa8d653f96641d4705c457aebaa187276e9f7537", size = 1581430, upload-time = "2026-08-10T14:15:06.853Z" },
{ url = "https://files.pythonhosted.org/packages/d4/29/d51b296e3191bb15d3d81ec375af1909e4466c0f395d744ed475801798a9/greenlet-3.5.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4441153ffba21b90d3ca89fe3d31f5c093ae6c0bf0cfdfc98f54cde22f95b62e", size = 1645684, upload-time = "2026-08-10T13:40:32.133Z" },
{ url = "https://files.pythonhosted.org/packages/12/63/369f1a1625e64e9e31df3963c6044056e3fdfa3fa3fdba3c54ffefa6e987/greenlet-3.5.5-cp313-cp313-win_amd64.whl", hash = "sha256:95c5b1f4b3a193f8a0c2de4bfdcb48d119f7f1063941f1de1f2168051b3e52dd", size = 324075, upload-time = "2026-08-10T13:26:58.974Z" },
{ url = "https://files.pythonhosted.org/packages/45/78/649cb5c09d4d81f6dd1444e75474a7206784743283a21d24171562ac4899/greenlet-3.5.5-cp313-cp313-win_arm64.whl", hash = "sha256:1af90aa4bc129883b340cdd6957a3bc74f60528a4993bbd1f53aaebe1d9981cc", size = 308260, upload-time = "2026-08-10T13:27:50.795Z" },
{ url = "https://files.pythonhosted.org/packages/7f/8c/080e881fa2be95ff1ddbd6994b2bab3b1a78df3b3fcab39306011764fcc7/greenlet-3.5.5-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d4a389a852e392a6366058651a20fa5ba40d979865aa81bea2ccbdc44805070d", size = 295309, upload-time = "2026-08-10T13:26:03.032Z" },
{ url = "https://files.pythonhosted.org/packages/25/cc/0ac614e6586c0e42d4cc281a5819150f4f43685744a4c5ff77139286409d/greenlet-3.5.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70b157cd319873e8b544ddc2de158f55bbd0a9b0218c8ce9332039801518e328", size = 661185, upload-time = "2026-08-10T14:14:37.867Z" },
{ url = "https://files.pythonhosted.org/packages/5e/b9/6808725354be8ad305dfe5172377664fc9642d4fc043be246b3314cf4482/greenlet-3.5.5-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8bdfd1424abcf26832961e766570cae79efdb9599d709088c9cb6ef82b194926", size = 673419, upload-time = "2026-08-10T14:27:28.652Z" },
{ url = "https://files.pythonhosted.org/packages/eb/52/f005d579acde46c3d1cc3cab1c9f3d5708c8a3006a4120e8cf5da801afe9/greenlet-3.5.5-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d98ef6f92e67c6dbf299dbfd8facc1b0d2d9cedf91e325e73b3d0373fe4309d8", size = 677863, upload-time = "2026-08-10T14:30:11.663Z" },
{ url = "https://files.pythonhosted.org/packages/42/2e/40c509967da7f254680826a2fa0dd22138ec79946c70b97542d74cde8b43/greenlet-3.5.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:182de51c6b572a705f2fafaab2e783bcf7d2760940229dfe73086cbae037af3e", size = 670822, upload-time = "2026-08-10T13:40:51.833Z" },
{ url = "https://files.pythonhosted.org/packages/c4/8a/a75f8a2bdcef3c358a3147cdc9db3aa83755f0a038f766ab0bedb66f512c/greenlet-3.5.5-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:159df1942d88e8f784cbb38d6f18bdb365cd11319cfbb3e89623de2b97892d53", size = 480554, upload-time = "2026-08-10T14:30:05.171Z" },
{ url = "https://files.pythonhosted.org/packages/2d/22/c3c2eee4a8fe191d6d1d183086c56133d646024e3d70bfd414829f64560b/greenlet-3.5.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8fec3f165dfe332e490c3247c0f6c23b0bfc45f06496ad7f00ddb00e3d35e4dc", size = 1628469, upload-time = "2026-08-10T14:15:08.11Z" },
{ url = "https://files.pythonhosted.org/packages/f7/87/25babd09b94cb1f03e71db815fde463f0262e40cfbd953d58a8d77311351/greenlet-3.5.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c6ce25fee6cabc8bf22cb8b52e642cbb821be5b9aec8094d07ff03378141b8e9", size = 1691952, upload-time = "2026-08-10T13:40:33.502Z" },
{ url = "https://files.pythonhosted.org/packages/2e/3d/5cc9701117ea4dc0eb7bf1f4f9b7888a6e2e5277ddfae095805ace50f2b6/greenlet-3.5.5-cp314-cp314-win_amd64.whl", hash = "sha256:7dffc5c859fe6059974df1e37d7923d654a83e2ae18fdd616994270e001115e1", size = 327458, upload-time = "2026-08-10T13:27:02.868Z" },
{ url = "https://files.pythonhosted.org/packages/a7/6b/594fa2de7fae7629168a404a4305d7d7e31a5742c50a801b1839543cb93d/greenlet-3.5.5-cp314-cp314-win_arm64.whl", hash = "sha256:5e2afcfc4d4305dd715809b03da5cbe437c8984f61d8917751eb5fe4aefa3e07", size = 311146, upload-time = "2026-08-10T13:27:25.046Z" },
{ url = "https://files.pythonhosted.org/packages/24/e0/50cd600b469e5734c72709b6b1838b6bc63f307b573c772c3132d6ecfe92/greenlet-3.5.5-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:0e5a7de979d764aea1f5b6e95cf92b5b37741b9823702041f34b126e7f690277", size = 305471, upload-time = "2026-08-10T13:26:20.568Z" },
{ url = "https://files.pythonhosted.org/packages/75/a3/77acd66dfc6387b5219b2080806c0cabb73c10eb1bb44b413c40a62015ba/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fef01bd457f11fc158b130ca0027a3c365693280e8e231b65bdaf57999f39f5b", size = 672470, upload-time = "2026-08-10T14:14:39.058Z" },
{ url = "https://files.pythonhosted.org/packages/b9/71/0d178142dca3ec19f46fb2212ae73d30ad53b9d548dc64804086033a7089/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5173a72310725a74afc82c164f0e52cb8ad0de62f2bb623f24f6c0cc07d80272", size = 679973, upload-time = "2026-08-10T14:27:30.072Z" },
{ url = "https://files.pythonhosted.org/packages/aa/ac/0d7887aa4bbfc9eba075cc428244dfc96f623478454d5ec81180d0d6bd5a/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5e9ec2e7c98e895fcea0c5cc57b2606cf86ece6d0a56578f3eb225e2af4f0387", size = 681587, upload-time = "2026-08-10T14:30:13.519Z" },
{ url = "https://files.pythonhosted.org/packages/6e/31/46eb8567302eaf787abf88d09df014e14ae3baf460af1b8b0efdbd3efcd5/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44f08341873200ba8a60a8bc14ace3d91f1754f7fa7bc66157714a8cd420a476", size = 676634, upload-time = "2026-08-10T13:40:53.004Z" },
{ url = "https://files.pythonhosted.org/packages/4f/18/8d58ba1c429b0383e3219a3d0e0bba241d0444d8ed05b73349953c7d7c7b/greenlet-3.5.5-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:102817506f6090b5176c746a82603341a549b40e5c3d5b72a4c672228a918c41", size = 510175, upload-time = "2026-08-10T14:30:07.047Z" },
{ url = "https://files.pythonhosted.org/packages/a3/e9/b88bbf5b29970cb84172dc2c32aa3e5e579ceb94c808e81c826454138850/greenlet-3.5.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d246c0db9a2513cd45f019ba178ea4d4d4705bd210ee465e2c15d76a1ab13874", size = 1637320, upload-time = "2026-08-10T14:15:09.317Z" },
{ url = "https://files.pythonhosted.org/packages/6d/8c/7631ed29cc6f0392f11830076e172ce4885e70b0bc2c1bce1731176d4b4e/greenlet-3.5.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:72507285b5caa1d17904a3f7c322ca780823a54170a0e04ec3f37bcc60d4db71", size = 1697412, upload-time = "2026-08-10T13:40:34.924Z" },
{ url = "https://files.pythonhosted.org/packages/da/0f/f7dd935f9c4cb1be49098770587f54d8a78518e55c89bce86c4fb4109057/greenlet-3.5.5-cp314-cp314t-win_amd64.whl", hash = "sha256:7805655781fb8f28a55d05fe57ed61f5f10f1892fb587673e3bb5264f28041f0", size = 331514, upload-time = "2026-08-10T13:29:20.611Z" },
{ url = "https://files.pythonhosted.org/packages/b7/e5/681b01f8fbc1b55232822f99e8f8afeb78a55a7c76a7bf9dbdc7ccb03a6d/greenlet-3.5.5-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:c0db80fcd5b8aece93f66c64f78a786bbb6b96c5fe63ef5a5a4581ecf8bab206", size = 295975, upload-time = "2026-08-10T13:28:45.985Z" },
{ url = "https://files.pythonhosted.org/packages/11/f2/69b488cd9e7267bf4b0fe8cdebf25d8d6df680d21bdf41150d23e23d6652/greenlet-3.5.5-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b241c32f912ada659808d68e308c568baf577eebf757d15471472de0c18cfad", size = 666823, upload-time = "2026-08-10T14:14:40.222Z" },
{ url = "https://files.pythonhosted.org/packages/84/d4/d5bc2fdebbdda0c94555925ba79948b8395d75a7f6a36cc85dce5bab9f11/greenlet-3.5.5-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ef6a08349401d8eaf3cb12688ac8557de95788556b8631ef17555a4a173022c0", size = 677613, upload-time = "2026-08-10T14:27:31.543Z" },
{ url = "https://files.pythonhosted.org/packages/65/53/4e13642efc4d7ad6554ecb2242a5be42666b2e1a067323e88dfc0124a04b/greenlet-3.5.5-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37faa97daccb6d9f4c2141ce3118d023c3c5506864a7d8bdf726f665018c1f76", size = 681436, upload-time = "2026-08-10T14:30:14.839Z" },
{ url = "https://files.pythonhosted.org/packages/bd/93/542d8a3a90f3b35c6ad8bf7e56a03010287f2cafa289a5b7985b5207db39/greenlet-3.5.5-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f2e3d061b8e13aec2f0441689b3c71b244a20e5d274a52cb0f7e31bd1d139552", size = 675930, upload-time = "2026-08-10T13:40:54.205Z" },
{ url = "https://files.pythonhosted.org/packages/cd/32/188447c9a468d6977d2989397226b0c6b65ab6f4cf943f931643328512fc/greenlet-3.5.5-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:b18007dc2473a7942fd157366b55f01da6fed7ce85318591005b419e0a439474", size = 487404, upload-time = "2026-08-10T14:30:08.903Z" },
{ url = "https://files.pythonhosted.org/packages/52/b5/89c9f2e8460d71101037d47a1feed11928615a5edd42370be290e0657eeb/greenlet-3.5.5-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:9ab5f5b93655e77fe0d6c2dfd22b5eac751bb1f876d8ec21761b7c1fb9266007", size = 1633878, upload-time = "2026-08-10T14:15:10.693Z" },
{ url = "https://files.pythonhosted.org/packages/b8/60/297de93f3b02ac78a5e04d32bb8bbe3080f4a73d8ed95016561463b70618/greenlet-3.5.5-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f0e5a21bd4452a88cf032fc43c4a5b307ab1380eacb63b5988f9c0317885e773", size = 1696597, upload-time = "2026-08-10T13:40:36.252Z" },
{ url = "https://files.pythonhosted.org/packages/18/25/54c6eaff4f337fb670215e89eb2d00d9499487b658e709d4b477be4a342e/greenlet-3.5.5-cp315-cp315-win_amd64.whl", hash = "sha256:469dbb0a78625642f4a626cfd0c6e8bccc0385b5e49189b6308bbe849ec88a8e", size = 327700, upload-time = "2026-08-10T13:28:06.752Z" },
{ url = "https://files.pythonhosted.org/packages/67/67/857e88a36301caa0e029870132c2478bd55d896630321432afab03a3115f/greenlet-3.5.5-cp315-cp315-win_arm64.whl", hash = "sha256:2d57406c3efd32d7a81e17a674314e8bd00792cdab49ea3228a49aa1bfb2e769", size = 311750, upload-time = "2026-08-10T13:34:08.815Z" },
{ url = "https://files.pythonhosted.org/packages/10/e2/3144c0a116067ac1e30457b0139a94d60d1d36a86e015de68e9ac87cb3bc/greenlet-3.5.5-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:68184dfcf50ccaa8e864770fe0633a7e27250ea9329f8192ef47ee9ecfd78e1c", size = 306387, upload-time = "2026-08-10T13:27:00.897Z" },
{ url = "https://files.pythonhosted.org/packages/5c/a1/cb4223a7e9b9f43b8807e8eb212358bfe2dfaa174a9ea2889eb1714dcba2/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ec0dc0e59dc9c61af5c47348365ccbbd7addfafe0a93b00336ff3da2907bdc6", size = 676472, upload-time = "2026-08-10T14:14:41.417Z" },
{ url = "https://files.pythonhosted.org/packages/9e/cd/a154b4498e5d8f12ada291cfb3b8d596eadde2177f5bf09a9be699d2a446/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e604f58e35833fc46ef20302bcb314dddbfd3fcf33a4f936216d51dd678d63ae", size = 684238, upload-time = "2026-08-10T14:27:32.946Z" },
{ url = "https://files.pythonhosted.org/packages/ce/f4/e450a68a152f819491d8c7df6a8254e761d87e6a78759268961f8c5bd4dd/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2888a3a38bc5ee5bb6c438372197152e815837e4fab7ed7a1f86ef18ffd58ad1", size = 686022, upload-time = "2026-08-10T14:30:15.96Z" },
{ url = "https://files.pythonhosted.org/packages/bf/bb/b0031d260c2968a3c87deebc51d80c64e499377f993aafe06ee3b7488cc2/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40239b5384f96da3963585cc6d7eaa9b56f8ae67e8d92cc82dd9e202fc847de3", size = 681246, upload-time = "2026-08-10T13:40:55.402Z" },
{ url = "https://files.pythonhosted.org/packages/18/23/17e63d6bf3b9c9b9dbea981b7f643a71f79603bdfb4f1c3a9cf353e22aed/greenlet-3.5.5-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:1e8d9391fe77f15649589a907cef972dbbd6352ef7ff7dc0492f658c0c26495f", size = 516951, upload-time = "2026-08-10T14:30:10.907Z" },
{ url = "https://files.pythonhosted.org/packages/9a/07/da554b71ab88e649da146e1065d86a48a5c5d92e50ab74ef41b504aa7f56/greenlet-3.5.5-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:a1eaccf5c3a1d3e46dead602c72e6836731e8e245c9de6a27764567b6b62d4c0", size = 1642735, upload-time = "2026-08-10T14:15:11.92Z" },
{ url = "https://files.pythonhosted.org/packages/78/76/26a3782a051677668af9d92beaa47cd87ba9dd5072f762961144a03dd4c6/greenlet-3.5.5-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:19e4e026fe20691f333b8eb1a3bc9625eceba8c3f9d62ec5a6f8581afbc6b5a5", size = 1700925, upload-time = "2026-08-10T13:40:37.656Z" },
{ url = "https://files.pythonhosted.org/packages/28/d9/fe7baf4190c2ae71f267efb9de21b3172bb35bc0ed1ef53dd6027d658e33/greenlet-3.5.5-cp315-cp315t-win_amd64.whl", hash = "sha256:712aee154f648bde84634654bb38bb78c69ac640c37a45c9effed800735049d8", size = 331829, upload-time = "2026-08-10T13:26:48.851Z" },
{ url = "https://files.pythonhosted.org/packages/df/af/419a4e383bd600858a9b67e9b280a60fdc383ee3f2fe5b6c0c1ef04e74d1/greenlet-3.5.5-cp315-cp315t-win_arm64.whl", hash = "sha256:7f049911ee81a16a03c33d5450d8d5867d27f596ca5fb201b86f4524e874468b", size = 315093, upload-time = "2026-08-10T13:29:34.949Z" },
]
[[package]] [[package]]
name = "idna" name = "idna"
version = "3.19" version = "3.19"
@@ -361,12 +447,19 @@ dependencies = [
{ name = "latex2mathml" }, { name = "latex2mathml" },
{ name = "lxml" }, { name = "lxml" },
{ name = "marko" }, { name = "marko" },
{ name = "mermaidx" },
{ name = "pillow" }, { name = "pillow" },
{ name = "playwright" },
{ name = "pygments" }, { name = "pygments" },
{ name = "python-docx" }, { name = "python-docx" },
{ name = "requests" }, { name = "requests" },
] ]
[package.optional-dependencies]
word = [
{ name = "pywin32", marker = "sys_platform == 'win32'" },
]
[package.dev-dependencies] [package.dev-dependencies]
dev = [ dev = [
{ name = "pytest" }, { name = "pytest" },
@@ -379,15 +472,32 @@ 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 = "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 = "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' and extra == 'word'", specifier = ">=306" },
{ name = "requests", specifier = ">=2.31.0,<3" }, { name = "requests", specifier = ">=2.31.0,<3" },
] ]
provides-extras = ["word"]
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [{ name = "pytest", specifier = ">=7.0" }] dev = [{ name = "pytest", specifier = ">=7.0" }]
[[package]]
name = "mermaidx"
version = "0.9.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "quickjs-ng" },
{ name = "resvg-py" },
{ name = "termaid" },
]
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" },
]
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "26.3" version = "26.3"
@@ -456,6 +566,25 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/52/3b/ce7a01026a7cf46e5452afa86f97a5e88ca97f562cafa76570178ab56d8d/pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5", size = 2554661, upload-time = "2024-07-01T09:48:20.293Z" }, { url = "https://files.pythonhosted.org/packages/52/3b/ce7a01026a7cf46e5452afa86f97a5e88ca97f562cafa76570178ab56d8d/pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5", size = 2554661, upload-time = "2024-07-01T09:48:20.293Z" },
] ]
[[package]]
name = "playwright"
version = "1.62.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "greenlet" },
{ name = "pyee" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/6c/5b/ca2abcf3aa69f9fb510215e3064f30b57fe57657c8d04ede45bb966d5606/playwright-1.62.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:d8da938f3748841a8754f2e1f0216902c1c8f8ae3720de8b32ccf8e6913a7c4f", size = 43732091, upload-time = "2026-07-31T17:00:44.178Z" },
{ url = "https://files.pythonhosted.org/packages/af/1a/0bfbe9904350961f4dbb713f04342e40d548c5fc26c8157bd13617c81492/playwright-1.62.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:db755ab27db21a04186f1fe8169888e42356086e439b1059b923ef417f0b6034", size = 42510842, upload-time = "2026-07-31T17:00:48.596Z" },
{ url = "https://files.pythonhosted.org/packages/66/dc/c0486b407ad0699a250f6bbe3066fca95344009a99ca66e88ca175c69dc1/playwright-1.62.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:5108bd5b3e87169ddf269feee097da5893af7f8aea4634dfc840518d64c1f1da", size = 43732093, upload-time = "2026-07-31T17:00:52.218Z" },
{ url = "https://files.pythonhosted.org/packages/43/6b/b24aebc2b04bffcb342bccf96e287c78b363e1615bed5cea97500cc0393a/playwright-1.62.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:ba33bae6a13b3d9d354c751cb618af357d20fe1d57767cbcce52079bbef17ad3", size = 47748926, upload-time = "2026-07-31T17:00:56.438Z" },
{ url = "https://files.pythonhosted.org/packages/36/43/b4b18bdc87e1949568fffdcde3ff9a0456266b2d0c6d4432cc34d89ea6eb/playwright-1.62.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db2d76613a57ad844362ce42f7d0c2fa26b19a4f7a46d4f76b891c631e6e5aff", size = 47441423, upload-time = "2026-07-31T17:01:00.404Z" },
{ url = "https://files.pythonhosted.org/packages/81/22/af5d926fc2c32a339eec00a443644bc40ab9db1dd2dd9017873c59773c0c/playwright-1.62.0-py3-none-win32.whl", hash = "sha256:e5614fa89355d7081457680324bb219f79f69c423c5cb6fa250e30b0d8aebf1c", size = 38164450, upload-time = "2026-07-31T17:01:04.187Z" },
{ url = "https://files.pythonhosted.org/packages/2b/a9/4160c1033c07af98bf841ad079457dd78408a5ee0dd56cbfe50b8b6a1c22/playwright-1.62.0-py3-none-win_amd64.whl", hash = "sha256:92c0d98ed04eb35af557b709875edba415b1f548bdb22ddb5bb3e1e6c835c2f1", size = 38164458, upload-time = "2026-07-31T17:01:08.459Z" },
{ url = "https://files.pythonhosted.org/packages/6c/ec/06b55d619a7082a766aa04f2c6bb31435c87f02930087d8a0517119408fa/playwright-1.62.0-py3-none-win_arm64.whl", hash = "sha256:ea8d3055aa9d5a9f1832ac82517bd8b42c78fac7ebcbebb0107116735c8cb6a1", size = 34208868, upload-time = "2026-07-31T17:01:11.818Z" },
]
[[package]] [[package]]
name = "pluggy" name = "pluggy"
version = "1.6.0" version = "1.6.0"
@@ -465,6 +594,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
] ]
[[package]]
name = "pyee"
version = "13.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8b/04/e7c1fe4dc78a6fdbfd6c337b1c3732ff543b8a397683ab38378447baa331/pyee-13.0.1.tar.gz", hash = "sha256:0b931f7c14535667ed4c7e0d531716368715e860b988770fc7eb8578d1f67fc8", size = 31655, upload-time = "2026-02-14T21:12:28.044Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/c4/b4d4827c93ef43c01f599ef31453ccc1c132b353284fc6c87d535c233129/pyee-13.0.1-py3-none-any.whl", hash = "sha256:af2f8fede4171ef667dfded53f96e2ed0d6e6bd7ee3bb46437f77e3b57689228", size = 15659, upload-time = "2026-02-14T21:12:26.263Z" },
]
[[package]] [[package]]
name = "pygments" name = "pygments"
version = "2.21.0" version = "2.21.0"
@@ -501,6 +642,58 @@ dependencies = [
] ]
sdist = { url = "https://files.pythonhosted.org/packages/8b/a0/52729ce4aa026f31b74cc877be1d11e4ddeaa361dc7aebec148171644b33/python-docx-0.8.11.tar.gz", hash = "sha256:1105d233a0956dd8dd1e710d20b159e2d72ac3c301041b95f4d4ceb3e0ebebc4", size = 5561613, upload-time = "2021-05-15T22:18:24.824Z" } sdist = { url = "https://files.pythonhosted.org/packages/8b/a0/52729ce4aa026f31b74cc877be1d11e4ddeaa361dc7aebec148171644b33/python-docx-0.8.11.tar.gz", hash = "sha256:1105d233a0956dd8dd1e710d20b159e2d72ac3c301041b95f4d4ceb3e0ebebc4", size = 5561613, upload-time = "2021-05-15T22:18:24.824Z" }
[[package]]
name = "pywin32"
version = "312"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fe/1b/9cfdeac80ee45bebbbcb31f1b7b99a0d81a1c72de48d837be984e0e88b1d/pywin32-312-cp310-cp310-win32.whl", hash = "sha256:772235332b5d1024c696f11cea1ae4be7930f0a8b894bb43db14e3f435f1ff7e", size = 6361387, upload-time = "2026-06-04T07:49:14.329Z" },
{ url = "https://files.pythonhosted.org/packages/33/b1/7afc96d041d982c27bc2df6f853d43f01fd273e3d39d04be3647ddeb533d/pywin32-312-cp310-cp310-win_amd64.whl", hash = "sha256:5dbc35d2b5320dc07f25fa31269cfb767471002b17de5eb067d03da68c7cb2db", size = 6926780, upload-time = "2026-06-04T07:49:16.881Z" },
{ url = "https://files.pythonhosted.org/packages/ce/3a/4140da9ad54108e517f4a16b2d83da3033e08662144623e1239587cb7db6/pywin32-312-cp310-cp310-win_arm64.whl", hash = "sha256:3020656e34f1cf7faeb7bccd2b84653a607c6ff0c55ada85e6487d61716deabd", size = 4307203, upload-time = "2026-06-04T07:49:18.993Z" },
{ url = "https://files.pythonhosted.org/packages/1f/f5/10a6e845a00fc5e7afd0a988b744f403d4d57162a28d160a093c4d9322f0/pywin32-312-cp311-cp311-win32.whl", hash = "sha256:17948aeadbdb091f0ced6ef0841620794e68327b94ee415571c1203594b7215c", size = 6362659, upload-time = "2026-06-04T07:49:21.349Z" },
{ url = "https://files.pythonhosted.org/packages/35/c4/dcd2d62b5944b6d5db53413a5899016ccd57ffcb7278f3f81655d25d2027/pywin32-312-cp311-cp311-win_amd64.whl", hash = "sha256:d11417d84412f859b722fad0841b3614459ed0047f7542d8362e77884f6b6e8a", size = 6928825, upload-time = "2026-06-04T07:49:23.934Z" },
{ url = "https://files.pythonhosted.org/packages/b7/56/3cbb433fe4501cdba2eb9040f56a4e1a8243faa4186b25295564d1a7a79d/pywin32-312-cp311-cp311-win_arm64.whl", hash = "sha256:b2200a054ca6d6625c4842fc56a4976a4b47f96b73dbe5538c3f813a80359f47", size = 6721875, upload-time = "2026-06-04T07:49:26.416Z" },
{ url = "https://files.pythonhosted.org/packages/83/ff/32aa7d2ed0ab12b323aaa64f9b75e6ad4f8fd09f9ccfc28c79414d46838d/pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b", size = 6371877, upload-time = "2026-06-04T07:49:28.836Z" },
{ url = "https://files.pythonhosted.org/packages/03/d9/77040d3b43df3f3be32ea289433d660d2727f5ba327bc73be835127d9d60/pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc", size = 6914841, upload-time = "2026-06-04T07:49:31.85Z" },
{ url = "https://files.pythonhosted.org/packages/e3/cc/7b1ec671775756020a0ee7f4feeaf3c568f0ab86bd3900088cf986937a92/pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950", size = 6727901, upload-time = "2026-06-04T07:49:34.244Z" },
{ url = "https://files.pythonhosted.org/packages/2d/41/12fbfd7f36ed2146d8bc9de96c2741296bf0d490b98508496cff322e274c/pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c", size = 6370184, upload-time = "2026-06-04T07:49:36.253Z" },
{ url = "https://files.pythonhosted.org/packages/ba/db/36a78e3403099d31d9746d13fdcde5accc43c1155f375a34d15983a479a7/pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9", size = 6914298, upload-time = "2026-06-04T07:49:38.876Z" },
{ url = "https://files.pythonhosted.org/packages/84/37/c1697194092b76de9ed47ca124323f02c57ffc8a45c06f88a3d5acaf01eb/pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831", size = 6727640, upload-time = "2026-06-04T07:49:41.083Z" },
{ url = "https://files.pythonhosted.org/packages/fc/2b/1f3cded5822fd49c02f40544cbb5f58c7cfd6b1694869fd476cb6170ee97/pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b", size = 6468928, upload-time = "2026-06-04T07:49:43.188Z" },
{ url = "https://files.pythonhosted.org/packages/21/82/3bf86d2e2808902013132e1ce905a7da0da53790f3836c64bf44d55e24f3/pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e", size = 7024157, upload-time = "2026-06-04T07:49:45.34Z" },
{ url = "https://files.pythonhosted.org/packages/a4/0e/73f6d6800b4f27655abd9e9f6aaeaefcddb2b946e4674efa2bab184a7f7b/pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa", size = 6839598, upload-time = "2026-06-04T07:49:47.613Z" },
{ url = "https://files.pythonhosted.org/packages/eb/61/caa39686032d2ebdd04ff0ab5cbe163126c0066d98e00c9018646e42393b/pywin32-312-cp315-cp315-win32.whl", hash = "sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed", size = 6471159, upload-time = "2026-06-04T07:49:50.035Z" },
{ url = "https://files.pythonhosted.org/packages/0f/cd/7e1de64a4a6f69c04214169657ccab0d93a670ea50e35eb8f489d7378249/pywin32-312-cp315-cp315-win_amd64.whl", hash = "sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5", size = 7025293, upload-time = "2026-06-04T07:49:54.857Z" },
{ url = "https://files.pythonhosted.org/packages/23/ed/4532e9388e65fa16b46776ef47ad631a64eda1631884488af707666350ed/pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9", size = 6840337, upload-time = "2026-06-04T07:49:57.531Z" },
]
[[package]]
name = "quickjs-ng"
version = "0.16.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/28/e1/0df75fc53a7764e0d99799aac01f4b72db1227f3e7dba2e5d2246b1222eb/quickjs_ng-0.16.2.1.tar.gz", hash = "sha256:cc0db5caeee8f93bcbbaf8ee59eefce58d33edc2c6ca340834e98c9520741ba1", size = 537108, upload-time = "2026-09-01T12:00:21.632Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/de/e1/e91954cfb83e8a0b4a064a50df4395394722cbfc090f6e98a126a3b029c5/quickjs_ng-0.16.2.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:5edf6aeb394d270ca7193279251a8b3e7e68577aed9f2ede7406a676056d6ffc", size = 531728, upload-time = "2026-09-01T11:59:53.519Z" },
{ url = "https://files.pythonhosted.org/packages/21/16/ed077e854cf64fc8eb76a81a9107765e65b042b31984f333ffec2bd8e1e0/quickjs_ng-0.16.2.1-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7446efb6134226d4d10236363c0b308a877613214ff2d798e4e270a8f22e579d", size = 2459036, upload-time = "2026-09-01T11:59:55.008Z" },
{ url = "https://files.pythonhosted.org/packages/1b/21/38ba3e548285fa591b7ecb9514bf65f411628e6030cf0f7505c4960bcaff/quickjs_ng-0.16.2.1-cp310-abi3-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:d65cda8ee4afb8ba0709e6881c0f90f483deb2239845a9a04491024aafc6184d", size = 2409184, upload-time = "2026-09-01T11:59:56.599Z" },
{ url = "https://files.pythonhosted.org/packages/65/52/c124d48efaf9d1f5b706c2d8366d6fb010873d8f447295f1c604825e5393/quickjs_ng-0.16.2.1-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:780bc24ba5f26b9dc6f9c737de9db8f9787c6a0ab72f91e0f778623fefce2d4d", size = 2537370, upload-time = "2026-09-01T11:59:58.585Z" },
{ url = "https://files.pythonhosted.org/packages/bf/54/a6758c8c775d7ca43d15d085aff2b10a6ff5dccf50bf3436cb52a197337e/quickjs_ng-0.16.2.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:50721415002975b3c64bf7836e64fd07a9d519396036257e8ef5ef419752c637", size = 2441594, upload-time = "2026-09-01T12:00:00.207Z" },
{ url = "https://files.pythonhosted.org/packages/53/f7/617c2aa3d4ebad27bc7b3d7140d0db89c40a5d28b494035bf3598ca86ed6/quickjs_ng-0.16.2.1-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:2b7a0f0412ea8e5874c659086a5f8b7d171d6ca2990b9c20e047bd11a25fb470", size = 2436556, upload-time = "2026-09-01T12:00:01.682Z" },
{ url = "https://files.pythonhosted.org/packages/9d/dc/12f6b77802a4b5a56e3f03756d21cc9247f3cd6399ccd3c51b2753d72aa5/quickjs_ng-0.16.2.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:c131d2fb5c9741ad2da9475807453dc98a31fde1df5f096e4037b1a7ae576025", size = 2527814, upload-time = "2026-09-01T12:00:03.318Z" },
{ url = "https://files.pythonhosted.org/packages/d2/c0/3d50214bb88361541712d15da7db0f291daa01294c540fc9f5f0d602c750/quickjs_ng-0.16.2.1-cp310-abi3-win32.whl", hash = "sha256:020eebb22ed98e3bd3ab44dc9225aff0186ee72df8a152d894b7ac55b079e295", size = 395822, upload-time = "2026-09-01T12:00:04.582Z" },
{ url = "https://files.pythonhosted.org/packages/57/f7/1b02fd4e531f31cdbe5fb2418b0b25587d3b721137fa9c7e457a003fd2d8/quickjs_ng-0.16.2.1-cp310-abi3-win_amd64.whl", hash = "sha256:7dd5f9fd78917c4c444c52eb2029f51610487bc219f5ca1b2766bcbd98b8afc3", size = 521986, upload-time = "2026-09-01T12:00:05.958Z" },
{ url = "https://files.pythonhosted.org/packages/9c/3b/ed008e94fd3683ee2aa68cb3675faf68b77e9d4922999888ade4cdca1b9b/quickjs_ng-0.16.2.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:7dacabcd25165e07397a19a5dc8877dc537792a6a00d47e4574922ee6ef74778", size = 475846, upload-time = "2026-09-01T12:00:07.3Z" },
{ url = "https://files.pythonhosted.org/packages/c5/50/46f4a7ea0a044ee1577e391f5a9188db5be848037305c4813487e003282e/quickjs_ng-0.16.2.1-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64e781335f02005a5df4f30276805c94a7d212d6769cd0118adce716e1012224", size = 522754, upload-time = "2026-09-01T12:00:08.724Z" },
{ url = "https://files.pythonhosted.org/packages/c2/0b/fa9bf4a1616403faf0d2cde932b814d58ed32c3e350f4566f964156d1a1f/quickjs_ng-0.16.2.1-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a089906381c0479a73f14dd46ee31c2ad26874b715e92c130bd08f51637b3b8d", size = 597645, upload-time = "2026-09-01T12:00:10.26Z" },
{ url = "https://files.pythonhosted.org/packages/62/b8/48da371ee5942d547c9c0135f2be5ddca4dac1036530eeb41611f6be128e/quickjs_ng-0.16.2.1-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d23daaec0e776f1e7c6fe0c55f396f06ff955635c4cc424e6e586158dca1436f", size = 555215, upload-time = "2026-09-01T12:00:11.602Z" },
{ url = "https://files.pythonhosted.org/packages/0e/b5/e2208efdee95fa86b1292b314a778b213aed917c3771259cd0814104efa7/quickjs_ng-0.16.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4b2818745645dec00a7659b81788654b2409a102e0e33a452353f6089b6ca807", size = 522051, upload-time = "2026-09-01T12:00:13.118Z" },
{ url = "https://files.pythonhosted.org/packages/6b/1c/e5ab0fe0bd8fd668e91511b580ea2a16e04c0fd354ac9ccf58f8ad5c99d3/quickjs_ng-0.16.2.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f1c4b9741fca4cd29e0212b293a5d3a9ecda85a87d66df7bcfbcf44bef282fa2", size = 475770, upload-time = "2026-09-01T12:00:14.397Z" },
{ url = "https://files.pythonhosted.org/packages/4e/4c/16a9c034989b1dbea312d0d5fd9e71af2465f5203e5140e1740bf5d43278/quickjs_ng-0.16.2.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:688942b54540df1bbca2c2c4f67a167df604df17bc76b95857b72507e195a31d", size = 522754, upload-time = "2026-09-01T12:00:15.97Z" },
{ url = "https://files.pythonhosted.org/packages/4c/63/6be5cc1d20d896f1069d362668362f689c55f5b2b2c88be3bc65fe070300/quickjs_ng-0.16.2.1-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:27771a34e74aa91fc739a60c6c8c5aa4eb1d38d8ca8a1fe03485bd91d1f27a86", size = 597645, upload-time = "2026-09-01T12:00:17.427Z" },
{ url = "https://files.pythonhosted.org/packages/ff/4c/dc282fbeba2e44f88a81cb55a993a8d30cb3de2682460d5262fdddf96baf/quickjs_ng-0.16.2.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:16e4b3272fd0ed98118570fdf5ac1880ad94a4563221e0c2508c0771c04ef033", size = 555216, upload-time = "2026-09-01T12:00:18.66Z" },
{ url = "https://files.pythonhosted.org/packages/78/fe/4581160df1d85c1ff092f656381e37aa0359fc4d15bad196e036e63adf94/quickjs_ng-0.16.2.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f275bfd4af674ed4c1becdf0c03c145d834fd9fb775c1c7eb9d162199710cd67", size = 522055, upload-time = "2026-09-01T12:00:20.372Z" },
]
[[package]] [[package]]
name = "requests" name = "requests"
version = "2.34.2" version = "2.34.2"
@@ -516,6 +709,71 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
] ]
[[package]]
name = "resvg-py"
version = "0.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/2a/64/a24f8f29d8bf158e01f6ccad68a1366afd922dc0f0977cbd0c0aaa7a22f2/resvg_py-0.5.0.tar.gz", hash = "sha256:6d3bf8e866b4e129524d9432a809138b2d100931d8d635bc81294002abcdfd46", size = 1756395, upload-time = "2026-08-24T19:43:27.663Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/47/89/49f7c84a2a3fc3d2b9973134f72f95467a40acfbc7ca5d820aeb06af6e79/resvg_py-0.5.0-cp310-abi3-android_24_arm64_v8a.whl", hash = "sha256:2715f2b88ce2cf91f57ff37bb34c5909c8007de431d488e9c3ebf6cf2d69c91b", size = 1370989, upload-time = "2026-08-24T19:42:09.747Z" },
{ url = "https://files.pythonhosted.org/packages/6b/d1/09ebd099134589225861e0668c9fdff103b0450df0e399689787a0d8962f/resvg_py-0.5.0-cp310-abi3-android_24_x86_64.whl", hash = "sha256:9901e2f9ce53e7535d2676123c8d4894bff040f52821e54192605b5dd4fb5af9", size = 1366983, upload-time = "2026-08-24T19:42:11.479Z" },
{ url = "https://files.pythonhosted.org/packages/ed/36/3408156e9cba54d1ef5793377f39be4096660933cc6df155ba425315bf09/resvg_py-0.5.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:9d3f5c2544d6b5f74847513e07e6ab6a70f9e7f0d8a141bc16bd4b0c555f4234", size = 1261946, upload-time = "2026-08-24T19:42:12.703Z" },
{ url = "https://files.pythonhosted.org/packages/74/bf/4083b177388125e5ce2ab9fa4cd9efd881fa133dd97e5b2d4ca68e543256/resvg_py-0.5.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:7b43f942157f5d16126e108dab8ab37e4bc2b198099e5f6274b753a3b1ac7b6e", size = 1216814, upload-time = "2026-08-24T19:42:13.943Z" },
{ url = "https://files.pythonhosted.org/packages/52/92/1dfd0d7b5f8dbb16f9c889bba0d7477ab514d1f2a81a5f904662215103bc/resvg_py-0.5.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e66216f78c84a27d34ce75f4535d8e26565771be4f1848ddc71e8a7ce78973a", size = 1414542, upload-time = "2026-08-24T19:42:15.538Z" },
{ url = "https://files.pythonhosted.org/packages/13/99/a77f933e6cc355fd168f6eae2e23b5d361cb61531bfb83477f9816a62a49/resvg_py-0.5.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:977921f22b0a3283e6cd121339a2aff51d0df3b542ce7a5f96fa3a87f8d65106", size = 1321567, upload-time = "2026-08-24T19:42:17.142Z" },
{ url = "https://files.pythonhosted.org/packages/8e/e0/a9d0cf6cee5fb1bf3abdba760821f76e1c979f81923c0bf54279dd1a285e/resvg_py-0.5.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9da8e52d7d5d16b288aa47fa830fd66301a6b6f135f9f37fa9f4854b7a722e6d", size = 1501716, upload-time = "2026-08-24T19:42:18.482Z" },
{ url = "https://files.pythonhosted.org/packages/5e/f2/cf7390e196923a0f591981d3f2754f70825f0a8b206d0778866806ba1159/resvg_py-0.5.0-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:200baa4a01b6779d7b6f3fa31e5eabfc5ab594a1317d75853ee73c5229686599", size = 1450509, upload-time = "2026-08-24T19:42:19.601Z" },
{ url = "https://files.pythonhosted.org/packages/9e/08/217f2289ceb16a4eafd9c9c6f69aa3221ef047a6abe4ff1ce5c8d6be87d8/resvg_py-0.5.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84f2378ecc7a8e38b03429efaefc816daec1b1970114909a6f973393b297c91b", size = 1405807, upload-time = "2026-08-24T19:42:20.75Z" },
{ url = "https://files.pythonhosted.org/packages/9b/d6/b3b9411b5b812799621ee43844552cbf7c0ddc2d5a552f5e91ba808ac67c/resvg_py-0.5.0-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9ebcc40941811b49001ad4e721aa87f489b74c0132ff3fcbceed97305c944749", size = 1491386, upload-time = "2026-08-24T19:42:22.131Z" },
{ url = "https://files.pythonhosted.org/packages/d8/e6/5d8e0fac79e19ec95db6902ab03f3e681a69183a0a959fb081a7385c9e7e/resvg_py-0.5.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7c3e8c2324fc2bcf03c1010b7987adf5ff4ce43e8fc1a7c9b8271cbbcca6ba37", size = 1593186, upload-time = "2026-08-24T19:42:24.057Z" },
{ url = "https://files.pythonhosted.org/packages/a4/81/db56ea6225d0294dfc5e96fa18d16231e33cd1812a75c4cf03e8e17586cc/resvg_py-0.5.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:4a5db1a607059a48f5d4c20c7363e4888e001b11a04465d36e3ca77a511651eb", size = 1600258, upload-time = "2026-08-24T19:42:26.11Z" },
{ url = "https://files.pythonhosted.org/packages/24/66/43c32a28e5d19ada46c8589cb3eaef5db0dc151be1aec0e86a68b9534ba7/resvg_py-0.5.0-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:d54a8c85e7d6f4ba55f39c2330c7830d8c98a7dc205ca3c2ca069f9b11cb01c4", size = 1653522, upload-time = "2026-08-24T19:42:27.674Z" },
{ url = "https://files.pythonhosted.org/packages/65/01/91794e3dedcfaf93b780ecd4cf0061262fd2665034756773f7e768812389/resvg_py-0.5.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:feee1ee6c2c0b64018c046a7604240c233c6cf14496e475238370c7d9a9db455", size = 1622060, upload-time = "2026-08-24T19:42:28.858Z" },
{ url = "https://files.pythonhosted.org/packages/dc/20/a7d7371a4104fd733702b942c396fd898510431ee3a1d2f7b4462da65117/resvg_py-0.5.0-cp310-abi3-win32.whl", hash = "sha256:45b2e66f76e7649155dc768c3cd1f5a94907d0086c2bde22da14c9ecbf9eda9a", size = 1204672, upload-time = "2026-08-24T19:42:30.191Z" },
{ url = "https://files.pythonhosted.org/packages/fe/53/aa8f92ce6eb2f97095d8b6359a1613c5a5ee0aa9b1a33434df9294362979/resvg_py-0.5.0-cp310-abi3-win_amd64.whl", hash = "sha256:1f6b8956c4143dbfe107bcd35799d0dfd778a40a8cd537893c0bf489898a6c3c", size = 1242277, upload-time = "2026-08-24T19:42:31.42Z" },
{ url = "https://files.pythonhosted.org/packages/56/64/e63614663df1404999802e82d462ffa534999c126067257bfba7d1de590c/resvg_py-0.5.0-cp310-abi3-win_arm64.whl", hash = "sha256:8016e2006c09953570af466e7674c398c1f255cb00022152b15e18f9e8ca3af8", size = 1153784, upload-time = "2026-08-24T19:42:32.607Z" },
{ url = "https://files.pythonhosted.org/packages/c2/1e/4e24cdabab6c4f9b2d1175fe6b57f07f24625a6a8e1ff331a2a4a28da3a6/resvg_py-0.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5547fc79ee600ee0e40ad01cdeb36140a74e85cfad2722973dae654db3667fcd", size = 1261363, upload-time = "2026-08-24T19:42:33.844Z" },
{ url = "https://files.pythonhosted.org/packages/d7/98/d4d0128dc2fd71eeaaa4e82d6c5a6213a89bcc96dd03b759fb7a5588c496/resvg_py-0.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4bd8da85e5332aded549894d7fe4aec6f19a681ecda3385acbfdf1a1c67bc8da", size = 1216436, upload-time = "2026-08-24T19:42:35.016Z" },
{ url = "https://files.pythonhosted.org/packages/4e/74/34fde2a05e81b6fd57445b18660fb7c3c2c988908cdf59f57f2481c98606/resvg_py-0.5.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1447c10535c4fa122bb20f702da5d23ad5053cdff831aa2d6f6b482ebab12485", size = 1413277, upload-time = "2026-08-24T19:42:36.295Z" },
{ url = "https://files.pythonhosted.org/packages/f5/e0/0225387a65b51a9e2a5b6a11a517e777b82e887db4b67e6e44d44607cf18/resvg_py-0.5.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c1ed526890579f8bf1afcf6c17f29c22659196c57b2c760e485f15dfc93dca64", size = 1320862, upload-time = "2026-08-24T19:42:37.701Z" },
{ url = "https://files.pythonhosted.org/packages/49/25/033b4ff263788ea10ae8e5ab2c445e8dcf0d78941b322781f8abe323dc73/resvg_py-0.5.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfe18bc3d36cc885190f450d5f0d26473c5cecb86b28bcb714f7a025e1cfd5c2", size = 1500068, upload-time = "2026-08-24T19:42:38.978Z" },
{ url = "https://files.pythonhosted.org/packages/d5/5a/472629604d6d0fbae13648d3ef378727b533e71baeff3403367b5efa9ae2/resvg_py-0.5.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8481cdbaf7fea5dbf2bfe57e86201c6a40193c462e365729185c66849b5966a", size = 1449571, upload-time = "2026-08-24T19:42:40.263Z" },
{ url = "https://files.pythonhosted.org/packages/8c/50/9776c9a2181205a21f90c1a14cd1deeacccb66d19457cf9b9cc25ebba17d/resvg_py-0.5.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12cdd0349ebd8efaade78f74fc3fc08fcdd3f21f7152fb199a561176a64581bf", size = 1404321, upload-time = "2026-08-24T19:42:41.391Z" },
{ url = "https://files.pythonhosted.org/packages/39/ec/78f53523b7c387312b0b790d33373d502eaf41310953baf2b17e18812d36/resvg_py-0.5.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c184cad5c3593dbe655ef9f304068fa941646947d94767dbd1afcbf094c8dae5", size = 1490813, upload-time = "2026-08-24T19:42:42.766Z" },
{ url = "https://files.pythonhosted.org/packages/e8/4e/ac6077896efb94d8c7ebe08552da48c323c657554e715c80f4e8e8f30cca/resvg_py-0.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:75e6822b492c66d85f03a6f2510ac69902ff0509a2a86cd50ef30ebb73c714ea", size = 1591632, upload-time = "2026-08-24T19:42:44.129Z" },
{ url = "https://files.pythonhosted.org/packages/3f/49/7dfe358ac7d52849b16ef98ad2cd4a41f77a87cff96122da095861fbb070/resvg_py-0.5.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1abaadac95daa2907e3fa0f668c90a099d4bfffe0a6fa7cf36d30c607bfd5797", size = 1599831, upload-time = "2026-08-24T19:42:45.373Z" },
{ url = "https://files.pythonhosted.org/packages/d4/d2/f53350c3b2c512ae9d6ab4ae39db20bb573048bfcc60fdd5213ab7474d81/resvg_py-0.5.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:92cdc56331224980b85c1604c7da9bef37737657cf123d88e920ca10a07c6a11", size = 1652833, upload-time = "2026-08-24T19:42:46.758Z" },
{ url = "https://files.pythonhosted.org/packages/ac/04/d958e02af538996ce963baa88d47667fc28c72b72aefea78546ab89a6288/resvg_py-0.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:403fed36186bbe4ef4eb3ec1d5fabe003a1c90ad71f145ca0e9b9f7f80e70696", size = 1620547, upload-time = "2026-08-24T19:42:48.173Z" },
{ url = "https://files.pythonhosted.org/packages/10/9d/8dc520a62512f309bc74dddee8309d8a940ae1ee80317825b5ef705d3b4e/resvg_py-0.5.0-cp314-cp314t-win32.whl", hash = "sha256:c7fad8f8c28e770da8783dc429bfa0d71f2abe740be2f8d726308a669a392919", size = 1204113, upload-time = "2026-08-24T19:42:49.398Z" },
{ url = "https://files.pythonhosted.org/packages/a2/0b/8edd6a94ed6c9d8306008c277c0e5f0d74df87cd2109eaff86ded437fcad/resvg_py-0.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b0284c50c7e3b009e97e5c64a2d31e02b8bb36cd066d947fda9e43f480ca19f7", size = 1240324, upload-time = "2026-08-24T19:42:50.87Z" },
{ url = "https://files.pythonhosted.org/packages/76/78/bdeb2fc44497c53c9f53e05acf58a571dc2589465031e37b9de8c0e57044/resvg_py-0.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5c026b67b79604f32865e132ef68ff25633b8668e35b29ad412fcef906c0c39a", size = 1152505, upload-time = "2026-08-24T19:42:52.06Z" },
{ url = "https://files.pythonhosted.org/packages/0e/d1/2f85ce0ec44642a849a57a709e121dd2fa934ea1a54d77bb31e8f4aea7e8/resvg_py-0.5.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:51fa0564ad1a3e82307c1aed7222b66edeb3b8c595251709f929b0a819109da1", size = 1261198, upload-time = "2026-08-24T19:42:53.279Z" },
{ url = "https://files.pythonhosted.org/packages/51/0c/b7af93cfd9bbcd83a4c8970e17dcf4917f12d3b88b695fa9a9b86913d375/resvg_py-0.5.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:1f91870d5315168093d546777fccece4406ad2053c1908f5ceab3c39f2c49e7c", size = 1216652, upload-time = "2026-08-24T19:42:54.876Z" },
{ url = "https://files.pythonhosted.org/packages/85/e8/2d6dbd6cf5be1871248d9e6307b4f42e916a16d83c13590ace9dccb8f49f/resvg_py-0.5.0-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d597eef189a8e728c8026417ea51b61720c83d2ed262b508362c4309cd57bc8a", size = 1412990, upload-time = "2026-08-24T19:42:56.491Z" },
{ url = "https://files.pythonhosted.org/packages/2d/10/c10989f4eebd61242134a0bc1e26a2eaf618cf911115e447ea570bfd9bdb/resvg_py-0.5.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5befa08450f4248b9670e054f446065d0fc33c1a4ff302baaacc205ddee97b3c", size = 1320774, upload-time = "2026-08-24T19:42:57.697Z" },
{ url = "https://files.pythonhosted.org/packages/de/ae/b6416f0d984a445d2ba962dd39750dd0f79c16346517f8f98b595bbdb37c/resvg_py-0.5.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17640c3bb2f4498a6aa61d256ec21257b32e68fd2564b509c4919f5171561b99", size = 1500202, upload-time = "2026-08-24T19:42:58.886Z" },
{ url = "https://files.pythonhosted.org/packages/5a/f2/f44bc28c82e3f21065a0b721ddae31769420747f99b807df83560dc75697/resvg_py-0.5.0-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fca2d6b28938e7fa7553a7f9c5f330b908c7fd8c50f4fe3d175ddcc5e958879", size = 1449452, upload-time = "2026-08-24T19:43:00.044Z" },
{ url = "https://files.pythonhosted.org/packages/e8/c9/c4cbcbbe45d327a669c4c346cdef9253a50e052c102da4fc92576e407041/resvg_py-0.5.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c4cc14543c29b753db751eace1dadcf0d58d77aa58be5370393bcfbcc1cbc2f", size = 1404433, upload-time = "2026-08-24T19:43:01.57Z" },
{ url = "https://files.pythonhosted.org/packages/60/03/7b7c89086cb7cbede4e21bcbbf2870d62564dcce71fc23fa97de67293ba5/resvg_py-0.5.0-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:869e4ab0b8f4a403d6fac93d2c4e3df79488b9f6fd053ba0f4fa4ed45d456fe5", size = 1490660, upload-time = "2026-08-24T19:43:03.026Z" },
{ url = "https://files.pythonhosted.org/packages/99/07/4a9595a3c760c91006ac4753ced8daabd2c6d64024ca668e2867bb84a283/resvg_py-0.5.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:f322d7bf0ddab60156d6cf1883718b726c1c210bd7623e253756986798c5c83e", size = 1591455, upload-time = "2026-08-24T19:43:04.404Z" },
{ url = "https://files.pythonhosted.org/packages/a2/7e/c2151824834b6df07083489a959cab2b37ba3b1834cc5e576aeb95e86e92/resvg_py-0.5.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:55d65708e2dee0de77cccc0d03d21cd148a491c2bc6ef25542081db8eee74923", size = 1599781, upload-time = "2026-08-24T19:43:05.666Z" },
{ url = "https://files.pythonhosted.org/packages/cd/ef/573c43420a5c39758f9e2cf67e8834ad430935eaecfb71c7ba73457b65c5/resvg_py-0.5.0-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:04b32b1e2d7a848124d9b96bc7446ceae71ea144d950007e93c4a382f7ee134c", size = 1652940, upload-time = "2026-08-24T19:43:06.993Z" },
{ url = "https://files.pythonhosted.org/packages/5e/76/68290af871f9347e1e8c7e14c8b09251362c74cff406b5f94c6711e8b6a2/resvg_py-0.5.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:7fc91829a4d12d80071e9f4f191a9f459adf310919cbdf1377711b30dfa996b5", size = 1620456, upload-time = "2026-08-24T19:43:08.422Z" },
{ url = "https://files.pythonhosted.org/packages/bb/af/28e4758e087c6d3a3e691ecd67fd1304074b9bca4b5f1563ab6d1336a6a4/resvg_py-0.5.0-cp315-abi3.abi3t-win32.whl", hash = "sha256:f0c834262db96eac4d5767e1025c21efefa0ed0359bded8dfd4b79fc7549694f", size = 1204143, upload-time = "2026-08-24T19:43:09.706Z" },
{ url = "https://files.pythonhosted.org/packages/73/5c/5b0e68ce15bd87eaee64501427437f57bece008e15bf40dd759563f0038a/resvg_py-0.5.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:011111a4c3f46d409e989fe88ec783a3ffae1f3877092ab0351aaf2167fe399d", size = 1240349, upload-time = "2026-08-24T19:43:11.397Z" },
{ url = "https://files.pythonhosted.org/packages/62/e6/25b6616cebbce1412bb5fe8b037545f382b4da875bc614a97ff1ecd7aa28/resvg_py-0.5.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:66e5a7699f2b00024ed7e95ec53df05bb3da277ee5bc86f867d487e310e4d392", size = 1152399, upload-time = "2026-08-24T19:43:12.618Z" },
{ url = "https://files.pythonhosted.org/packages/af/9b/fb611193ffdb8e4a84e2c43a6b06d27ad90287c06f115eaefab0a00555b6/resvg_py-0.5.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cf22e9feaa41ac4eb781ec266b685d001bd02dccd9c28b74ca9ed2cc755891c", size = 1414851, upload-time = "2026-08-24T19:43:14.259Z" },
{ url = "https://files.pythonhosted.org/packages/43/41/eac0a093095c591851f94c78d6ef28ea493e0509a3ad40ab22f35252ab51/resvg_py-0.5.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3651dfe44c05bf3c594d2f074af06ba49a1adb0c11ed2e62f0a0ae5647d4e689", size = 1321840, upload-time = "2026-08-24T19:43:15.521Z" },
{ url = "https://files.pythonhosted.org/packages/d8/6e/6ad270a3fb33779a21a2005704cfced4a246f2be3ac494ae7862de05cc90/resvg_py-0.5.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e3ae9f72f7a265953c3cec67dbb849d76fe9391658820a2fd05769d858869fdd", size = 1502443, upload-time = "2026-08-24T19:43:16.868Z" },
{ url = "https://files.pythonhosted.org/packages/c9/fa/60a35163617fbbc0d4e59c290c1e85e4b0b1dc6c044033e58fb269e22215/resvg_py-0.5.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:efa682ad33f8d2fa22cc1034e606fed5eee09b516c09a889b01ef07ce80a07fe", size = 1450915, upload-time = "2026-08-24T19:43:18.02Z" },
{ url = "https://files.pythonhosted.org/packages/b0/fc/8e12645ac88089047ca701f41dfbb59a5746124141ce5b0932b9b8fd1b7f/resvg_py-0.5.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:326358d1a83fba3c2c373576f15ad2b4f5bc6e90aad167682e39e3e15ea72c31", size = 1406198, upload-time = "2026-08-24T19:43:19.226Z" },
{ url = "https://files.pythonhosted.org/packages/46/7b/6c2defb6c83442efaaf983d948c2339b2c65fa2a4a00028a1942d190c0f5/resvg_py-0.5.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4beec3f2a6c59b8c3f2fa45dd167392c8074a326debad9277cb305a514546be8", size = 1491831, upload-time = "2026-08-24T19:43:20.716Z" },
{ url = "https://files.pythonhosted.org/packages/28/ff/b3d111c01b620f0319c7f9a0f84ca95a14edaf8a5d9816aa0584d3323785/resvg_py-0.5.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:a9f5583cf9f3d806ee802b948bf0632acd680661ca4f6e8007eac75ac3f09e1e", size = 1593658, upload-time = "2026-08-24T19:43:22.257Z" },
{ url = "https://files.pythonhosted.org/packages/99/0d/652bc5ac43d1eb95fc4190c62a2442c7fe717446315d1b5dfb5fb1dafa38/resvg_py-0.5.0-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:c113a655f558cd1d62616a459ad7ad61072cafdb3c997c9d8f83077a0d186af9", size = 1600562, upload-time = "2026-08-24T19:43:23.566Z" },
{ url = "https://files.pythonhosted.org/packages/15/45/3ef71b7426b937e14dcebfb4f11f29f7d92f294e17bf1419ffcc817dd92e/resvg_py-0.5.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:e9bbb65e6a969fc792b6bcff7d7f203ab58775475cf30db4577a05f62be72904", size = 1654199, upload-time = "2026-08-24T19:43:24.883Z" },
{ url = "https://files.pythonhosted.org/packages/b6/56/fa3137277cb3b4e697b2d0625769105c7825cf61d8855cccfd1768c04726/resvg_py-0.5.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:c2a493b6ada049cdee60b5ebe81f1eec8d36766c38962dadfd8d0ec8e5201cae", size = 1622519, upload-time = "2026-08-24T19:43:26.146Z" },
]
[[package]] [[package]]
name = "six" name = "six"
version = "1.17.0" version = "1.17.0"
@@ -525,6 +783,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
] ]
[[package]]
name = "termaid"
version = "0.8.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/02/74/951acc74226adf541b876257787fbf1f8a68d81f2d6a59749165b51dba44/termaid-0.8.0.tar.gz", hash = "sha256:58c4b8960bd4382b1b13c79f1beb9a53ebcbc58531fc8ecea8c64a307112f377", size = 401844, upload-time = "2026-07-29T08:52:05.975Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/05/e6/2f1cdca71adb5864a97ad06c0f0b509e9ab9592b3da83f2dd5708a61ec68/termaid-0.8.0-py3-none-any.whl", hash = "sha256:3b572ed705bbca77eeffcbc1906a73f1c8119613a0552bb2ff7463a2672b1cb3", size = 159955, upload-time = "2026-07-29T08:52:04.707Z" },
]
[[package]] [[package]]
name = "tomli" name = "tomli"
version = "2.4.1" version = "2.4.1"