- add Local Render Mermaid - add page-starе для указания смещения страниц - add Гиперссылки в документе на списки литературы
This commit is contained in:
+4
-1
@@ -32,9 +32,12 @@ if errorlevel 1 goto :pipfail
|
||||
|
||||
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
|
||||
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 ...
|
||||
python -m PyInstaller --noconfirm %PYI_CLEAN% --workpath build-fast --distpath dist md2gost.fast.spec
|
||||
|
||||
+8
-2
@@ -19,10 +19,16 @@ if errorlevel 1 goto :pipfail
|
||||
|
||||
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
|
||||
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 подцепит консоль сам ...
|
||||
|
||||
+6
-4
@@ -29,10 +29,12 @@ python -m md2gost report.docx --check-pages # только проверка в
|
||||
| `--table-continuation` | Основные | `word` (по умолчанию), `off`, `legacy`, `caption`, `soft` |
|
||||
| `--listing-continuation` | Основные | то же для листингов |
|
||||
| `--table-repeat-header` | Галочка | шапка на фрагментах после word-split |
|
||||
| `--plantuml-jar` / `--kroki-url` | Настройки → Диаграммы | локальный рендер |
|
||||
| `--diagram-fallback` | Диаграммы | `remote` / `local` / `off` |
|
||||
| `--plantuml-jar` / `--kroki-url` | Настройки → Диаграммы | локальный рендер UML / запасной Kroki |
|
||||
| `--diagram-fallback` | Диаграммы | `remote` / `local` / `off` (отключает только kroki.io) |
|
||||
| `--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` |
|
||||
| `--styles` | Настройки → Файлы → Стили JSON | оверлей `md2gost.styles.json` (также рядом с `.md`); см. [styles.md](styles.md) |
|
||||
| `--syntax-highlighting` | (CLI) | подсветка в листингах |
|
||||
@@ -45,7 +47,7 @@ DOCX не умеет сам писать «Продолжение Таблицы
|
||||
## Меню GUI
|
||||
|
||||
- **Настройки → Файлы** — шаблон, титул, задание, стили JSON, выходной путь
|
||||
- **Настройки → Диаграммы** — PlantUML, Kroki, формат, кэш includes
|
||||
- **Настройки → Диаграммы** — PlantUML, Mermaid (Chromium), Kroki, формат, кэш includes
|
||||
- **Шаблоны UML** — редактор `md2gost.schemes.json`
|
||||
- **Справка → Инструкция / Документация / Схемы / Промпт для ИИ**
|
||||
(Документация — встроенный просмотр `docs/*.md`, вшито в exe)
|
||||
|
||||
+1
-1
@@ -115,7 +115,7 @@ $$
|
||||
|
||||
## Источники
|
||||
|
||||
В тексте: `[1]`, `[2, 3]` (для ВКР — `[1.5]`).
|
||||
В тексте: `[1]`, `[2, 3]` (для ВКР — `[1.5]`). В DOCX номера становятся ссылками на пункты списка источников.
|
||||
|
||||
В списке:
|
||||
|
||||
|
||||
+9
-4
@@ -1,6 +1,6 @@
|
||||
# Схемы и диаграммы
|
||||
|
||||
В отчёте блок кода с языком диаграммы превращается в **Рисунок** (PNG или SVG). PNG руками экспортировать не нужно: md2gost вызывает PlantUML (jar / Kroki) или Kroki для Mermaid.
|
||||
В отчёте блок кода с языком диаграммы превращается в **Рисунок** (PNG или SVG). PNG руками экспортировать не нужно: md2gost вызывает PlantUML (jar / Kroki) или локальный Mermaid (браузер / QuickJS), иначе Kroki.
|
||||
|
||||
## Синтаксис в markdown
|
||||
|
||||
@@ -31,7 +31,7 @@ Rel(user, app, "логин")
|
||||
|---------|----------------|
|
||||
| `` ```uml `` / `` ```plantuml `` | Сырой PlantUML (`@startuml`…`@enduml`) |
|
||||
| `` ```uml-<id> `` / `` ```<id> `` | Схема из `md2gost.schemes.json` (обёртка и `!include` добавляются сами) |
|
||||
| `` ```mermaid `` / `` ```mmd `` | Mermaid через Kroki |
|
||||
| `` ```mermaid `` / `` ```mmd `` | Mermaid: браузер / QuickJS / Kroki |
|
||||
| `` ```bpmn `` / `` ```uml-bpmn `` | BPMN 2.0 (макросы библиотеки md2gost) |
|
||||
|
||||
**IDEF0 / DFD** конвертер не рисует — вставляйте готовый PNG как обычный Рисунок.
|
||||
@@ -107,9 +107,14 @@ id схемы: латиница, цифры, `_`; начинается с бук
|
||||
2. Локальный Kroki (`KROKI_URL` / `--kroki-url`, по умолчанию `http://localhost:8000`)
|
||||
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/`.
|
||||
Кэш includes: `md2gost.include-cache.json` + папка `include-cache/` (сброс в GUI: Настройки → Диаграммы / Шаблоны UML).
|
||||
|
||||
+15
-1
@@ -44,8 +44,13 @@ datas += collect_data_files("docx")
|
||||
# Explicit mpl-data so DejaVu + matplotlib font cache files are always in the exe.
|
||||
# hook-matplotlib.py also collects this; PyInstaller dedupes by dest path.
|
||||
datas += collect_data_files("matplotlib", includes=["mpl-data/**"])
|
||||
for _pkg in ("mermaidx", "termaid", "quickjs", "quickjs_ng", "resvg_py", "playwright"):
|
||||
try:
|
||||
datas += collect_data_files(_pkg)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for pkg in ("lxml", "freetype"):
|
||||
for pkg in ("lxml", "freetype", "quickjs", "resvg_py"):
|
||||
try:
|
||||
binaries += collect_dynamic_libs(pkg)
|
||||
except Exception:
|
||||
@@ -78,6 +83,15 @@ hiddenimports += [
|
||||
"tkinter.filedialog",
|
||||
"tkinter.messagebox",
|
||||
"tkinter.ttk",
|
||||
"mermaidx",
|
||||
"mermaidx.diagram",
|
||||
"mermaidx.engines",
|
||||
"mermaidx.engines.quickjs_engine",
|
||||
"termaid",
|
||||
"quickjs",
|
||||
"resvg_py",
|
||||
"playwright",
|
||||
"playwright.sync_api",
|
||||
]
|
||||
# Do NOT collect_submodules("pygments.lexers") here — hook-pygments.py already
|
||||
# collects all lexers/formatters/styles once during Analysis.
|
||||
|
||||
+15
-1
@@ -27,8 +27,13 @@ datas += collect_data_files("lxml")
|
||||
datas += collect_data_files("latex2mathml")
|
||||
datas += collect_data_files("docx")
|
||||
datas += collect_data_files("matplotlib", includes=["mpl-data/**"])
|
||||
for _pkg in ("mermaidx", "termaid", "quickjs", "quickjs_ng", "resvg_py", "playwright"):
|
||||
try:
|
||||
datas += collect_data_files(_pkg)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for pkg in ("lxml", "freetype"):
|
||||
for pkg in ("lxml", "freetype", "quickjs", "resvg_py"):
|
||||
try:
|
||||
binaries += collect_dynamic_libs(pkg)
|
||||
except Exception:
|
||||
@@ -61,6 +66,15 @@ hiddenimports += [
|
||||
"tkinter.filedialog",
|
||||
"tkinter.messagebox",
|
||||
"tkinter.ttk",
|
||||
"mermaidx",
|
||||
"mermaidx.diagram",
|
||||
"mermaidx.engines",
|
||||
"mermaidx.engines.quickjs_engine",
|
||||
"termaid",
|
||||
"quickjs",
|
||||
"resvg_py",
|
||||
"playwright",
|
||||
"playwright.sync_api",
|
||||
]
|
||||
hiddenimports += collect_submodules("pygments.lexers")
|
||||
hiddenimports += collect_submodules("marko")
|
||||
|
||||
+11
-4
@@ -104,7 +104,7 @@ python -m md2gost report.md -o report.docx --toc manual
|
||||
| Листинг | `%id Подпись` перед code fence |
|
||||
| Диаграмма UML / Mermaid / схемы | `%id Подпись` + ````uml` / ````uml-c4` / ````bpmn` / ````mermaid` → Рисунок; `+listing` — ещё и Листинг. Схемы в `md2gost.schemes.json`. IDEF0 нет |
|
||||
| Формула | `%eq1` + `$$…$$` + `@Формула:eq1` (номер только при ссылке) |
|
||||
| Источник | `[1]` в тексте; `[1]: …` в списке |
|
||||
| Источник | `[1]` в тексте (кликабельная ссылка на пункт списка); `[1]: …` в списке |
|
||||
| Разрыв страницы | `---` на отдельной строке + `--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
|
||||
StartMessage(s, "заявка")
|
||||
@@ -243,9 +243,14 @@ DefaultFlow(gw, e_no)
|
||||
2. Локальный Kroki (`KROKI_URL` / `--kroki-url`, по умолчанию `http://localhost:8000`)
|
||||
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-страница, затем снова книжная.
|
||||
|
||||
@@ -255,6 +260,8 @@ Mermaid всегда идёт через Kroki (шаги 2–3).
|
||||
|
||||
```bash
|
||||
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-format svg
|
||||
```
|
||||
|
||||
+49
-2
@@ -147,7 +147,7 @@ def build_parser() -> ArgumentParser:
|
||||
"--diagram-fallback",
|
||||
choices=["local", "remote", "off"],
|
||||
default="remote",
|
||||
help="Если локальный рендер UML недоступен: remote (kroki.io), local (ошибка), off",
|
||||
help="Если локальный рендер недоступен: remote (kroki.io), local (ошибка), off",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--diagram-format",
|
||||
@@ -159,7 +159,28 @@ def build_parser() -> ArgumentParser:
|
||||
"--diagram-scale",
|
||||
type=float,
|
||||
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(
|
||||
"--schemes",
|
||||
@@ -206,6 +227,11 @@ def request_from_args(args) -> ConvertRequest:
|
||||
diagram_scale=float(args.diagram_scale),
|
||||
schemes_path=args.schemes_path,
|
||||
styles_path=getattr(args, "styles_path", None),
|
||||
page_number_start=(
|
||||
int(args.page_start)
|
||||
if getattr(args, "page_start", None) is not None and int(args.page_start) >= 1
|
||||
else None
|
||||
),
|
||||
debug=args.debug,
|
||||
open_when_done=bool(args.debug),
|
||||
)
|
||||
@@ -218,6 +244,8 @@ def _argv_needs_console(argv: list[str]) -> bool:
|
||||
return False
|
||||
if "-h" in args or "--help" in args:
|
||||
return True
|
||||
if "--install-chromium" in args:
|
||||
return True
|
||||
if "--gui" in args:
|
||||
return False
|
||||
return any(not a.startswith("-") for a in args)
|
||||
@@ -247,6 +275,25 @@ def main():
|
||||
_enable_windows_console()
|
||||
parser = build_parser()
|
||||
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)
|
||||
|
||||
if should_launch_gui(args.filename, args.gui):
|
||||
|
||||
@@ -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
|
||||
class BiblioEntry:
|
||||
key: str
|
||||
|
||||
@@ -41,13 +41,16 @@ class Bibliography(Renderable):
|
||||
|
||||
@staticmethod
|
||||
def _make_entry(parent: Parented, e: BiblioEntry) -> Paragraph:
|
||||
from .bibliography import biblio_bookmark_name
|
||||
|
||||
p = Paragraph(parent)
|
||||
try:
|
||||
p.style = "Bibliography"
|
||||
except KeyError:
|
||||
pass
|
||||
# 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
|
||||
|
||||
def render(self, previous_rendered: RenderedInfo, layout_state: LayoutState) -> Generator[
|
||||
|
||||
+125
-25
@@ -94,6 +94,13 @@ def configure_diagrams(
|
||||
global _CONFIG
|
||||
ensure_user_schemes()
|
||||
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"
|
||||
scale = DEFAULT_DIAGRAM_SCALE if diagram_scale is None else float(diagram_scale)
|
||||
if scale < 1:
|
||||
@@ -128,13 +135,19 @@ def _cache_root(cache_dir: str | None = None) -> Path:
|
||||
return path
|
||||
|
||||
|
||||
def _cache_digest(source: str) -> str:
|
||||
return hashlib.sha256(source.encode("utf-8")).hexdigest()[:24]
|
||||
def _cache_digest(source: str, *, cache_prefix: str = "") -> str:
|
||||
payload = (cache_prefix + source) if cache_prefix else source
|
||||
return hashlib.sha256(payload.encode("utf-8")).hexdigest()[:24]
|
||||
|
||||
|
||||
def _cache_paths(source: str, cache_dir: str | None = None) -> tuple[Path, Path]:
|
||||
def _cache_paths(
|
||||
source: str,
|
||||
cache_dir: str | None = None,
|
||||
*,
|
||||
cache_prefix: str = "",
|
||||
) -> tuple[Path, Path]:
|
||||
root = _cache_root(cache_dir)
|
||||
digest = _cache_digest(source)
|
||||
digest = _cache_digest(source, cache_prefix=cache_prefix)
|
||||
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) "
|
||||
"или оставьте как есть — картинки нарисует интернет (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:
|
||||
@@ -394,7 +414,32 @@ def render_diagram(
|
||||
prepared = _ensure_plantuml_png_scale(prepared, scale)
|
||||
if prepared != before:
|
||||
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 = (
|
||||
diagram_format
|
||||
if diagram_format is not None
|
||||
@@ -402,29 +447,57 @@ def render_diagram(
|
||||
)
|
||||
want_svg = fmt == "svg"
|
||||
svg_target = out_svg_path if want_svg else None
|
||||
kroki_svg_target = kroki_svg_path if want_svg else None
|
||||
|
||||
if out_png.is_file() and out_png.stat().st_size > 0:
|
||||
has_svg = out_svg_path.is_file() and out_svg_path.stat().st_size > 0
|
||||
if not want_svg or has_svg:
|
||||
hit_scale = pixel_scale
|
||||
if kroki_type == "mermaid" and scale > 1 and local_prefix != cache_prefix:
|
||||
hit_scale = scale
|
||||
return DiagramRenderResult(
|
||||
png_path=str(out_png),
|
||||
svg_path=str(out_svg_path) if (want_svg and has_svg) else None,
|
||||
pixel_scale=pixel_scale,
|
||||
pixel_scale=hit_scale,
|
||||
)
|
||||
# PNG cached but SVG missing in svg mode — fall through to fill SVG.
|
||||
|
||||
# Mermaid: also accept unscaled Kroki cache hit
|
||||
if (
|
||||
kroki_type == "mermaid"
|
||||
and kroki_png != out_png
|
||||
and kroki_png.is_file()
|
||||
and kroki_png.stat().st_size > 0
|
||||
):
|
||||
has_svg = kroki_svg_path.is_file() and kroki_svg_path.stat().st_size > 0
|
||||
if not want_svg or has_svg:
|
||||
return DiagramRenderResult(
|
||||
png_path=str(kroki_png),
|
||||
svg_path=str(kroki_svg_path) if (want_svg and has_svg) else None,
|
||||
pixel_scale=1.0,
|
||||
)
|
||||
|
||||
jar = plantuml_jar if plantuml_jar is not None else _CONFIG.plantuml_jar
|
||||
jar = resolve_plantuml_jar(jar or None, download=False)
|
||||
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
|
||||
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
|
||||
if want_svg and out_svg_path.is_file() and out_svg_path.stat().st_size > 0:
|
||||
svg = str(out_svg_path)
|
||||
if want_svg and use_svg_path.is_file() and use_svg_path.stat().st_size > 0:
|
||||
svg = str(use_svg_path)
|
||||
return DiagramRenderResult(
|
||||
png_path=str(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
|
||||
@@ -432,16 +505,41 @@ def render_diagram(
|
||||
if _render_plantuml_jar(prepared, out_png, jar or "", out_svg=svg_target):
|
||||
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
|
||||
if _render_kroki_pair(prepared, kroki_type, local_kroki, out_png, svg_target):
|
||||
return _result()
|
||||
if _render_kroki_pair(prepared, kroki_type, local_kroki, kroki_png, kroki_svg_target):
|
||||
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:
|
||||
if out_png.is_file() and out_png.stat().st_size > 0:
|
||||
_log.warning("Диаграмма: SVG недоступен, вставляю только PNG")
|
||||
return DiagramRenderResult(
|
||||
png_path=str(out_png), svg_path=None, pixel_scale=pixel_scale,
|
||||
)
|
||||
for candidate in (out_png, kroki_png):
|
||||
if candidate.is_file() and candidate.stat().st_size > 0:
|
||||
_log.warning("Диаграмма: SVG недоступен, вставляю только PNG")
|
||||
pscale = pixel_scale
|
||||
if (
|
||||
kroki_type == "mermaid"
|
||||
and candidate == out_png
|
||||
and scale > 1
|
||||
and local_prefix != cache_prefix
|
||||
):
|
||||
pscale = scale
|
||||
elif kroki_type == "mermaid" and candidate == kroki_png:
|
||||
pscale = 1.0
|
||||
return DiagramRenderResult(
|
||||
png_path=str(candidate), svg_path=None, pixel_scale=pscale,
|
||||
)
|
||||
raise RuntimeError(message)
|
||||
|
||||
if mode == "off":
|
||||
@@ -451,8 +549,8 @@ def render_diagram(
|
||||
"(задайте --plantuml-jar или KROKI_URL; remote fallback отключён)"
|
||||
)
|
||||
return _png_fallback_or_raise(
|
||||
"Не удалось отрендерить диаграмму через локальный Kroki "
|
||||
"(задайте --kroki-url / KROKI_URL; remote fallback отключён)"
|
||||
"Не удалось отрендерить диаграмму локально "
|
||||
"(браузер / QuickJS / --kroki-url; remote fallback отключён)"
|
||||
)
|
||||
if mode == "local":
|
||||
if kroki_type == "plantuml":
|
||||
@@ -461,8 +559,8 @@ def render_diagram(
|
||||
"(Java+plantuml.jar или локальный Kroki)"
|
||||
)
|
||||
return _png_fallback_or_raise(
|
||||
"Локальный Kroki недоступен для диаграммы "
|
||||
f"(тип {kroki_type}; задайте работающий --kroki-url)"
|
||||
"Локальный рендер Mermaid недоступен "
|
||||
f"(браузер / Chromium / QuickJS / Kroki; тип {kroki_type})"
|
||||
)
|
||||
|
||||
# 3) Remote fallback
|
||||
@@ -470,11 +568,13 @@ def render_diagram(
|
||||
"Диаграмма: локальный рендер недоступен, использую remote %s",
|
||||
REMOTE_KROKI_URL,
|
||||
)
|
||||
if _render_kroki_pair(prepared, kroki_type, REMOTE_KROKI_URL, out_png, svg_target):
|
||||
return _result()
|
||||
if _render_kroki_pair(prepared, kroki_type, REMOTE_KROKI_URL, kroki_png, kroki_svg_target):
|
||||
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(
|
||||
"Не удалось отрендерить диаграмму (PlantUML/Kroki)"
|
||||
"Не удалось отрендерить диаграмму (PlantUML/Mermaid/Kroki)"
|
||||
)
|
||||
|
||||
|
||||
|
||||
Vendored
+2314
File diff suppressed because one or more lines are too long
@@ -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
@@ -65,7 +65,7 @@ TABLE_LABELS = {
|
||||
}
|
||||
FALLBACK_LABELS = {
|
||||
"remote": "Авто: локально, иначе интернет (kroki.io)",
|
||||
"local": "Только этот компьютер (нужна Java)",
|
||||
"local": "Только этот компьютер (без kroki.io)",
|
||||
"off": "Не рисовать диаграммы",
|
||||
}
|
||||
FORMAT_LABELS = {
|
||||
@@ -257,6 +257,7 @@ class Md2GostApp:
|
||||
self.plantuml_var = tk.StringVar()
|
||||
self.kroki_var = tk.StringVar()
|
||||
self.scale_var = tk.StringVar(value="2")
|
||||
self.page_start_var = tk.StringVar(value="")
|
||||
self.highlight_var = tk.BooleanVar(value=False)
|
||||
self.debug_var = tk.BooleanVar(value=False)
|
||||
self.output_var.trace_add("write", self._on_output_edited)
|
||||
@@ -351,8 +352,8 @@ class Md2GostApp:
|
||||
win = tk.Toplevel(self.root)
|
||||
win.title("Настройки — Диаграммы")
|
||||
win.configure(bg=PAPER)
|
||||
win.minsize(560, 420)
|
||||
win.geometry("680x520")
|
||||
win.minsize(560, 480)
|
||||
win.geometry("700x600")
|
||||
win.transient(self.root)
|
||||
extra = ttk.Frame(win, padding=12)
|
||||
extra.pack(fill=tk.BOTH, expand=True)
|
||||
@@ -363,7 +364,8 @@ class Md2GostApp:
|
||||
text=(
|
||||
"Блоки ```uml / ```uml-c4 / ```bpmn / ```mermaid сами станут рисунками.\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",
|
||||
wraplength=640,
|
||||
@@ -375,16 +377,25 @@ class Md2GostApp:
|
||||
status_row.grid(row=1, column=0, columnspan=2, sticky="ew", pady=(0, 8))
|
||||
status_row.columnconfigure(0, weight=1)
|
||||
self.diag_status = ttk.Label(status_row, text="", style="Hint.TLabel", wraplength=480, justify=tk.LEFT)
|
||||
self.diag_status.grid(row=0, column=0, sticky="w")
|
||||
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.grid(row=0, column=1, sticky="e", padx=(8, 0))
|
||||
_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(
|
||||
extra, 2, "Как рисовать",
|
||||
self.fallback_var,
|
||||
_combo_values(FALLBACK_LABELS, ("remote", "local", "off")),
|
||||
tip="Локально (PlantUML+Java), через интернет или не рисовать",
|
||||
tip="Локально (PlantUML / браузер / QuickJS), через интернет или не рисовать",
|
||||
)
|
||||
self._combo(
|
||||
extra, 3, "Формат схем",
|
||||
@@ -393,12 +404,12 @@ class Md2GostApp:
|
||||
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.grid(row=4, column=1, sticky="w", pady=3)
|
||||
scale_entry = ttk.Entry(scale_row, textvariable=self.scale_var, width=8)
|
||||
scale_entry.pack(side=tk.LEFT)
|
||||
_tip(scale_entry, "Качество растра PlantUML; на странице размер как при 1")
|
||||
_tip(scale_entry, "Качество растра PlantUML / локального Mermaid; на странице размер как при 1")
|
||||
ttk.Label(
|
||||
scale_row,
|
||||
text=" качество; на странице размер как при 1",
|
||||
@@ -422,7 +433,10 @@ class Md2GostApp:
|
||||
tip="Пусто — вшитый jar или скачанный автоматически")
|
||||
ttk.Label(
|
||||
extra,
|
||||
text="Свой Kroki — только если у вас крутится свой сервер. Пусто = сначала localhost:8000, потом kroki.io.",
|
||||
text=(
|
||||
"Свой Kroki — запасной путь (и для UML без Java). Пусто = localhost:8000, потом kroki.io. "
|
||||
"Mermaid сначала рисуется локально (браузер / QuickJS)."
|
||||
),
|
||||
style="Hint.TLabel",
|
||||
wraplength=640,
|
||||
).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),
|
||||
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.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 = [
|
||||
("Заменять «—» на «-»", self.emdash_var, "Типографское тире → дефис (методичка обычно требует «—»)"),
|
||||
("--- → разрыв страницы", self.hr_pagebreak_var, "Строка --- станет page break в Word"),
|
||||
@@ -1260,6 +1290,45 @@ class Md2GostApp:
|
||||
"Можно ничего не делать — диаграммы нарисует 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:
|
||||
path = filedialog.askopenfilename(title=title, filetypes=types)
|
||||
if path:
|
||||
@@ -1300,6 +1369,8 @@ class Md2GostApp:
|
||||
self.strict_var.set(bool(req.strict))
|
||||
self.check_pages_var.set(bool(getattr(req, "check_pages", 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.debug_var.set(bool(req.debug))
|
||||
self._debug_next = bool(req.debug)
|
||||
@@ -1333,6 +1404,7 @@ class Md2GostApp:
|
||||
check_only=self.check_only_var.get(),
|
||||
check_pages=self.check_pages_var.get(),
|
||||
table_repeat_header=self.table_repeat_header_var.get(),
|
||||
page_number_start=self._parse_page_start(),
|
||||
strict=self.strict_var.get(),
|
||||
syntax_highlighting=self.highlight_var.get(),
|
||||
plantuml_jar=self.plantuml_var.get().strip() or None,
|
||||
@@ -1345,6 +1417,16 @@ class Md2GostApp:
|
||||
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:
|
||||
raw = (self.scale_var.get() or "2").strip().replace(",", ".")
|
||||
try:
|
||||
|
||||
@@ -13,7 +13,7 @@ USAGE_HELP = """md2gost — Markdown → DOCX (ТЗ МИРЭА / ГОСТ 7.32)
|
||||
1. Перетащите .md в верхнюю область (или кликните по ней).
|
||||
2. Выберите тип документа и параметры в блоке «Основные».
|
||||
Шаблон / титул / задание — Настройки → Файлы.
|
||||
PlantUML / Kroki — Настройки → Диаграммы.
|
||||
PlantUML / Mermaid / Kroki — Настройки → Диаграммы.
|
||||
Свои UML-схемы — меню «Шаблоны UML».
|
||||
3. Нажмите «Конвертировать». Документ сохранится рядом с исходником (или по пути «Выходной DOCX»).
|
||||
Если файл уже есть и вы откажетесь перезаписывать — сохранится как имя_гггг-мм-дд-ЧЧ-ММ.docx.
|
||||
@@ -24,6 +24,7 @@ USAGE_HELP = """md2gost — Markdown → DOCX (ТЗ МИРЭА / ГОСТ 7.32)
|
||||
Полезные галочки
|
||||
• «—» → «-» — заменить типографское тире на дефис (по умолчанию **выкл.**; методичка требует «—»).
|
||||
• --- → разрыв страницы — по умолчанию выкл. (строка --- игнорируется). Вкл. — page break в Word.
|
||||
• Смещение страниц — начальный номер PAGE в теле (пусто = сквозной; 1 = тело с 1).
|
||||
• Проверить по ТЗ — замечания по структуре, «рис.», источникам.
|
||||
• Проверить вёрстку в Word — полупустые страницы (эвристика, возможны ложные срабатывания; нужны Word + pywin32).
|
||||
• Титул / задание — отдельные DOCX, вставляются перед телом отчёта.
|
||||
@@ -93,7 +94,7 @@ CLI (тот же движок)
|
||||
…
|
||||
```
|
||||
|
||||
Mermaid (через Kroki, свой URL или kroki.io):
|
||||
Mermaid (локально: браузер / QuickJS; иначе Kroki):
|
||||
%seq Последовательность +listing
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
@@ -104,7 +105,7 @@ CLI (тот же движок)
|
||||
+landscape — отдельная альбомная страница (A4, повёрт на 90°) вокруг рисунка/таблицы, затем снова книжная.
|
||||
Языки: uml, plantuml, mermaid / mmd, или uml-<id> / <id> из файла схем (c4, usecase, bpmn, …).
|
||||
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 как обычный Рисунок.
|
||||
Подробнее — Справка → Схемы и раздел ниже в инструкции.
|
||||
|
||||
@@ -114,7 +115,7 @@ IDEF0 конвертер не рисует — вставляйте готовы
|
||||
См. @Формула:eq1
|
||||
|
||||
Источники
|
||||
В тексте: [1]
|
||||
В тексте: [1] (в DOCX — ссылка на пункт списка)
|
||||
В списке: [1]: Иванов И. И. Название. — М.: Наука, 2023. — 120 с.
|
||||
|
||||
Разрыв страницы
|
||||
@@ -157,7 +158,7 @@ SCHEMES_HELP = """СХЕМЫ ДИАГРАММ (PlantUML)
|
||||
```uml-c4 — схема с id «c4» (то же, что ```c4)
|
||||
```uml-usecase — схема «usecase»
|
||||
```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.
|
||||
|
||||
Пример
|
||||
@@ -225,8 +226,9 @@ BPMN 2.0 (```bpmn)
|
||||
|
||||
Рендер
|
||||
UML: Java + plantuml.jar (локально) → иначе локальный Kroki → иначе kroki.io.
|
||||
Mermaid: только Kroki (свой --kroki-url / localhost / kroki.io).
|
||||
Формат: PNG по умолчанию (PlantUML ~scale 2 для чёткости, размер на странице как при 1; --diagram-scale); --diagram-format svg — PNG + SVG (svgBlip в Word 2016+).
|
||||
Mermaid: системный Chrome/Edge → Playwright Chromium (кнопка / --install-chromium) →
|
||||
QuickJS (mermaidx, офлайн) → локальный Kroki → kroki.io.
|
||||
Формат: PNG по умолчанию (PlantUML / локальный Mermaid ~scale 2 для чёткости, размер на странице как при 1; --diagram-scale); --diagram-format svg — PNG + SVG (svgBlip в Word 2016+).
|
||||
+landscape у %подписи — альбомная страница под широкий рисунок/таблицу.
|
||||
"""
|
||||
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
@@ -156,3 +156,38 @@ def ensure_continuous_page_numbers(document) -> None:
|
||||
# Keep continuous: remove start attribute if present
|
||||
if child.get(qn("w:start")) is not None:
|
||||
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))
|
||||
|
||||
@@ -56,6 +56,8 @@ class ConvertRequest:
|
||||
open_when_done: bool = False
|
||||
check_pages: bool = False
|
||||
table_repeat_header: bool = False
|
||||
# PAGE field start on body section; None = continuous from document start.
|
||||
page_number_start: int | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -121,6 +123,20 @@ def _fix_front_matter_after_compose(document, *, had_title: bool, had_assignment
|
||||
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:
|
||||
"""Run check and/or conversion. Does not re-raise; errors go into ConvertResult."""
|
||||
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),
|
||||
)
|
||||
|
||||
_apply_page_offset(
|
||||
document,
|
||||
req,
|
||||
had_title=bool(req.title),
|
||||
had_assignment=bool(req.assignment),
|
||||
)
|
||||
|
||||
document.core_properties.author = getuser()
|
||||
document.core_properties.comments = "Создано при помощи md2gost (ТЗ МИРЭА)"
|
||||
try:
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
from copy import copy
|
||||
from typing import Generator, Any
|
||||
from typing import Generator
|
||||
|
||||
from docx.shared import Length, Parented, RGBColor
|
||||
from docx.text.paragraph import Paragraph as DocxParagraph
|
||||
from docx.text.paragraph import Run as DocxRun
|
||||
from docx.enum.text import WD_LINE_SPACING
|
||||
from docx.opc.constants import RELATIONSHIP_TYPE
|
||||
|
||||
from . import Renderable
|
||||
@@ -17,6 +16,14 @@ from ..util import create_element
|
||||
from ..rendered_info import RenderedInfo
|
||||
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:
|
||||
def __init__(self, url, docx_paragraph: DocxParagraph):
|
||||
@@ -48,6 +55,39 @@ class Link:
|
||||
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):
|
||||
def __init__(self, parent: Parented):
|
||||
self._parent = parent
|
||||
@@ -70,6 +110,73 @@ class Paragraph(Renderable):
|
||||
self._docx_paragraph.add_run()._element.\
|
||||
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):
|
||||
"""Placeholder run resolved later via resolve_pending_refs()."""
|
||||
run = self._docx_paragraph.add_run(f"{type_}?")
|
||||
|
||||
@@ -37,10 +37,23 @@ class RenderableFactory:
|
||||
classes = []
|
||||
for child in children:
|
||||
if isinstance(child, (extended_markdown.RawText, extended_markdown.Literal)):
|
||||
paragraph_or_link.add_run(child.children,
|
||||
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)
|
||||
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_italic=extended_markdown.Emphasis 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):
|
||||
paragraph_or_link.add_run(child.children, is_italic=True)
|
||||
elif isinstance(child, extended_markdown.Image):
|
||||
|
||||
@@ -14,6 +14,8 @@ dependencies = [
|
||||
"latex2mathml>=3.76.0,<4",
|
||||
"pygments>=2.16.1,<3",
|
||||
"lxml>=4.9,<6",
|
||||
"mermaidx>=0.9.0,<1",
|
||||
"playwright>=1.40.0,<2",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
@@ -75,6 +77,8 @@ matplotlib = { version = "3.7.2", platform = "!= linux" }
|
||||
requests = "^2.31.0"
|
||||
latex2mathml = "^3.76.0"
|
||||
pygments = "^2.16.1"
|
||||
mermaidx = "^0.9.0"
|
||||
playwright = "^1.40.0"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
md2gost = "md2gost.__main__:main"
|
||||
|
||||
@@ -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
|
||||
@@ -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())
|
||||
@@ -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
|
||||
@@ -356,6 +356,9 @@ def test_render_mermaid_skips_jar(tmp_path):
|
||||
calls["jar"] += 1
|
||||
return False
|
||||
|
||||
def fake_local(*args, **kwargs):
|
||||
return False
|
||||
|
||||
def fake_kroki(source, diagram_type, base_url, out_path, fmt="png"):
|
||||
calls["kroki"].append((diagram_type, fmt, base_url))
|
||||
if fmt == "png":
|
||||
@@ -366,6 +369,7 @@ def test_render_mermaid_skips_jar(tmp_path):
|
||||
|
||||
configure_diagrams(fallback="remote", cache_dir=str(tmp_path))
|
||||
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):
|
||||
result = render_diagram("mermaid", "flowchart LR\nA-->B", cache_dir=str(tmp_path))
|
||||
assert calls["jar"] == 0
|
||||
|
||||
@@ -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")
|
||||
@@ -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
|
||||
@@ -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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "idna"
|
||||
version = "3.19"
|
||||
@@ -361,12 +447,19 @@ dependencies = [
|
||||
{ name = "latex2mathml" },
|
||||
{ name = "lxml" },
|
||||
{ name = "marko" },
|
||||
{ name = "mermaidx" },
|
||||
{ name = "pillow" },
|
||||
{ name = "playwright" },
|
||||
{ name = "pygments" },
|
||||
{ name = "python-docx" },
|
||||
{ name = "requests" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
word = [
|
||||
{ name = "pywin32", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "pytest" },
|
||||
@@ -379,15 +472,32 @@ requires-dist = [
|
||||
{ name = "latex2mathml", specifier = ">=3.76.0,<4" },
|
||||
{ name = "lxml", specifier = ">=4.9,<6" },
|
||||
{ name = "marko", specifier = ">=2.0.0,<3" },
|
||||
{ name = "mermaidx", specifier = ">=0.9.0,<1" },
|
||||
{ name = "pillow", specifier = ">=10.0.0,<11" },
|
||||
{ name = "playwright", specifier = ">=1.40.0,<2" },
|
||||
{ name = "pygments", specifier = ">=2.16.1,<3" },
|
||||
{ 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" },
|
||||
]
|
||||
provides-extras = ["word"]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
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]]
|
||||
name = "packaging"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "pluggy"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "pygments"
|
||||
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" }
|
||||
|
||||
[[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]]
|
||||
name = "requests"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "six"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "tomli"
|
||||
version = "2.4.1"
|
||||
|
||||
Reference in New Issue
Block a user