Files
Igor20264 510f7e7adf
Python application / build (push) Waiting to run
v0.5.2
Что то сделал
2026-09-08 19:37:54 +03:00

390 lines
16 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python
from argparse import ArgumentParser, BooleanOptionalAction
import sys
from .doc_metadata import AUTHOR_SOURCES, DEFAULT_DOC_COMMENTS
from .pipeline import ConvertRequest, convert, should_launch_gui
from .profiles import (
DEFAULT_HEADING_NUMBERING,
DEFAULT_TABLE_CONTINUATION,
DEFAULT_LISTING_CONTINUATION,
DEFAULT_TOC_MODE,
DOC_TYPES,
HEADING_NUMBERING_MODES,
TABLE_CONTINUATION_MODES,
LISTING_CONTINUATION_MODES,
TOC_MODES,
)
def build_parser() -> ArgumentParser:
parser = ArgumentParser(
prog="md2gost",
description=(
"Генерация DOCX-отчётов из Markdown по ТЗ МИРЭА / ГОСТ. "
"Типы: coursework/practice/vkr, APID_coursework, PIS_custom. "
"Без файла или с --gui открывается окно. "
"DOCX → Markdown: python -m word2md файл.docx (или md2gost файл.docx). "
"FODT: python -m md2fodt …"
),
)
parser.add_argument(
"filename", nargs="?",
help="Путь до .md (сборка DOCX), .docx (импорт в Markdown) или .docx с --check-pages",
)
parser.add_argument(
"--gui", action="store_true",
help="Открыть графический интерфейс (можно сразу передать .md)",
)
parser.add_argument("-o", "--output", help="Путь до сгенерированного .docx")
parser.add_argument("-t", "--template", help="Путь до шаблона .docx")
parser.add_argument(
"--type", dest="doc_type", choices=DOC_TYPES, default="practice",
help="Тип: practice | coursework | vkr | PIS_custom | APID_coursework (по умолчанию practice)",
)
parser.add_argument(
"--heading-numbering",
choices=HEADING_NUMBERING_MODES,
default=DEFAULT_HEADING_NUMBERING,
help=(
"Нумерация заголовков разделов: "
"manual — цифры из markdown (# 1 … / ## 1.1 …), автонумерация Word отключена; "
"auto — нумерует Word, цифры в начале заголовка md снимаются. "
f"По умолчанию: {DEFAULT_HEADING_NUMBERING}."
),
)
parser.add_argument(
"--toc",
choices=TOC_MODES,
default=DEFAULT_TOC_MODE,
help=(
"Содержание: native — встроенное поле Word TOC (обновить при открытии); "
"manual — собрать в md2gost с номерами страниц из layout. "
f"По умолчанию: {DEFAULT_TOC_MODE}."
),
)
parser.add_argument(
"--table-continuation",
choices=TABLE_CONTINUATION_MODES,
default=DEFAULT_TABLE_CONTINUATION,
help=(
"Таблицы длиннее страницы: "
"word — после сохранения Word COM режет по реальной пагинации + «Продолжение…» "
"(по умолчанию; нужны Windows, Word, pywin32); "
"off/soft — одна таблица, пагинация Word, без авто«Продолжение»; "
"legacy/caption — режем по оценке высоты и вставляем «Продолжение…» "
"(оценка ≠ Word, возможны артефакты). "
f"По умолчанию: {DEFAULT_TABLE_CONTINUATION}."
),
)
parser.add_argument(
"--listing-continuation",
choices=LISTING_CONTINUATION_MODES,
default=DEFAULT_LISTING_CONTINUATION,
help=(
"Листинги длиннее страницы: "
"word — после сохранения Word COM + «Продолжение Листинга…» "
"(по умолчанию; нужны Windows, Word, pywin32); "
"off/soft — один блок, пагинация Word, без авто«Продолжение»; "
"legacy/caption — режем по оценке высоты и вставляем «Продолжение Листинга…» "
"(оценка ≠ Word, возможны артефакты). "
f"По умолчанию: {DEFAULT_LISTING_CONTINUATION}."
),
)
parser.add_argument(
"--table-repeat-header",
action=BooleanOptionalAction,
default=False,
help=(
"При --table-continuation word: повторять шапку таблицы на каждом фрагменте "
"после разрыва. По умолчанию выкл."
),
)
parser.add_argument(
"--emdash-to-hyphen",
action=BooleanOptionalAction,
default=False,
help=(
"Автозамена типографского тире «—» на дефис «-» в тексте и подписях. "
"По умолчанию выключено (тире «—» по методичке); включить: --emdash-to-hyphen."
),
)
parser.add_argument(
"--hr-pagebreak",
action=BooleanOptionalAction,
default=False,
help=(
"Строка «---» / «***» / «___» — разрыв страницы Word. "
"По умолчанию такие строки игнорируются; включить: --hr-pagebreak."
),
)
parser.add_argument("--title", help="DOCX титульного листа (вставляется перед телом; "
"отключает автогенерацию)")
parser.add_argument(
"--auto-title",
action=BooleanOptionalAction,
default=False,
help=(
"Сгенерировать титул из info_conv.yaml / блока ```title (нужны ФИО и группа). "
"По умолчанию выкл.; включить: --auto-title. Явный --title отключает генератор."
),
)
parser.add_argument("--assignment", help="DOCX бланка задания")
parser.add_argument(
"--student",
help="ФИО студента для автотитула (иначе md2gost.user.json)",
)
parser.add_argument(
"--group",
help="Учебная группа для автотитула (иначе md2gost.user.json)",
)
parser.add_argument(
"--doc-author-from",
choices=AUTHOR_SOURCES,
default=None,
help=(
"Автор в свойствах DOCX: os — имя пользователя ОС; "
"student — ФИО из профиля; custom — --doc-author / md2gost.user.json. "
"Иначе берётся из Настройки → Метаданные (по умолчанию os)."
),
)
parser.add_argument(
"--doc-author",
default=None,
help="Автор в свойствах DOCX (перебивает --doc-author-from)",
)
parser.add_argument(
"--doc-last-modified-by",
default=None,
help="Поле «Кем изменён» в свойствах DOCX; пусто — как автор",
)
parser.add_argument(
"--doc-title",
default=None,
help="Название в свойствах DOCX (File → Info)",
)
parser.add_argument(
"--doc-subject",
default=None,
help="Тема в свойствах DOCX",
)
parser.add_argument(
"--doc-keywords",
default=None,
help="Ключевые слова (теги) в свойствах DOCX",
)
parser.add_argument(
"--doc-comments",
default=None,
help=(
"Примечание в свойствах DOCX. "
f"По умолчанию: «{DEFAULT_DOC_COMMENTS}». "
"Пустая строка — не писать примечание."
),
)
parser.add_argument(
"--doc-category",
default=None,
help="Категория в свойствах DOCX",
)
parser.add_argument("--check", help="Проверить markdown по ТЗ и вывести отчёт",
action="store_true")
parser.add_argument("--check-only", help="Только проверка, без генерации документа",
action="store_true")
parser.add_argument(
"--check-pages",
help=(
"После конвертации (или для готового .docx) проверить полупустые страницы "
"через Microsoft Word. Эвристика — возможны ложные срабатывания. "
"Нужны Windows, Word и pywin32."
),
action="store_true",
)
parser.add_argument("--strict", help="Код выхода 1 при ошибках проверки",
action="store_true")
parser.add_argument("--syntax-highlighting", help="Подсветка синтаксиса в листингах",
action=BooleanOptionalAction)
parser.add_argument(
"--plantuml-jar",
help="Путь к plantuml.jar (иначе env PLANTUML_JAR)",
)
parser.add_argument(
"--kroki-url",
help="URL локального Kroki (иначе env KROKI_URL, default http://localhost:8000)",
)
parser.add_argument(
"--diagram-fallback",
choices=["local", "remote", "off"],
default="remote",
help="Если локальный рендер недоступен: remote (kroki.io), local (ошибка), off",
)
parser.add_argument(
"--diagram-format",
choices=["png", "svg"],
default="png",
help="Формат схем в Word: png (по умолчанию) или svg (вектор + PNG-запасной, Word 2016+)",
)
parser.add_argument(
"--diagram-scale",
type=float,
default=2.0,
help=(
"Масштаб растра PlantUML / локального Mermaid (качество); "
"размер на странице как при 1. По умолчанию 2"
),
)
parser.add_argument(
"--page-start",
type=int,
default=None,
metavar="N",
help=(
"Смещение / начальный номер страницы в колонтитуле тела документа. "
"Пусто — сквозная нумерация Word; например 1 — тело начинается с 1"
),
)
parser.add_argument(
"--install-chromium",
action="store_true",
help=(
"Скачать headless Chromium для локального Mermaid "
"в %%LOCALAPPDATA%%\\md2gost\\ms-playwright (и выйти)"
),
)
parser.add_argument(
"--schemes",
dest="schemes_path",
help="Путь к md2gost.schemes.json (иначе рядом с приложением / с .md)",
)
parser.add_argument(
"--styles",
dest="styles_path",
help=(
"Путь к md2gost.styles.json (оверлей оформления поверх пресета --type; "
"также подхватывается md2gost.styles.json рядом с .md)"
),
)
parser.add_argument("--debug", help="Добавляет отладочные данные в документ",
action="store_true")
return parser
def request_from_args(args) -> ConvertRequest:
return ConvertRequest(
filename=args.filename or "",
output=args.output,
template=args.template,
doc_type=args.doc_type,
heading_numbering=args.heading_numbering,
toc_mode=args.toc,
table_continuation=args.table_continuation,
listing_continuation=args.listing_continuation,
table_repeat_header=bool(getattr(args, "table_repeat_header", False)),
emdash_to_hyphen=args.emdash_to_hyphen,
hr_pagebreak=args.hr_pagebreak,
title=args.title,
assignment=args.assignment,
student=getattr(args, "student", None),
group=getattr(args, "group", None),
doc_author_source=getattr(args, "doc_author_from", None),
doc_author=getattr(args, "doc_author", None),
doc_last_modified_by=getattr(args, "doc_last_modified_by", None),
doc_title=getattr(args, "doc_title", None),
doc_subject=getattr(args, "doc_subject", None),
doc_keywords=getattr(args, "doc_keywords", None),
doc_comments=getattr(args, "doc_comments", None),
doc_category=getattr(args, "doc_category", None),
auto_title=bool(getattr(args, "auto_title", False)),
check=args.check,
check_only=args.check_only,
check_pages=bool(getattr(args, "check_pages", False)),
strict=args.strict,
syntax_highlighting=bool(args.syntax_highlighting),
plantuml_jar=args.plantuml_jar,
kroki_url=args.kroki_url,
diagram_fallback=args.diagram_fallback,
diagram_format=args.diagram_format,
diagram_scale=float(args.diagram_scale),
schemes_path=args.schemes_path,
styles_path=getattr(args, "styles_path", None),
page_number_start=(
int(args.page_start)
if getattr(args, "page_start", None) is not None and int(args.page_start) >= 1
else None
),
debug=args.debug,
open_when_done=bool(args.debug),
)
def _argv_needs_console(argv: list[str]) -> bool:
"""Frozen windowed exe: attach a console for CLI / --help, not for GUI."""
args = argv[1:]
if not args:
return False
if "-h" in args or "--help" in args:
return True
if "--install-chromium" in args:
return True
if "--gui" in args:
return False
return any(not a.startswith("-") for a in args)
def _enable_windows_console() -> None:
if sys.platform != "win32" or not getattr(sys, "frozen", False):
return
try:
import ctypes
kernel32 = ctypes.windll.kernel32
if not kernel32.AttachConsole(0xFFFFFFFF):
if not kernel32.GetConsoleWindow():
kernel32.AllocConsole()
sys.stdout = open("CONOUT$", "w", encoding="utf-8", errors="replace")
sys.stderr = open("CONOUT$", "w", encoding="utf-8", errors="replace")
try:
sys.stdin = open("CONIN$", "r", encoding="utf-8", errors="replace")
except OSError:
pass
except Exception:
pass
def main():
if _argv_needs_console(sys.argv):
_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):
from .gui import run_gui
run_gui(req)
return
result = convert(req)
sys.exit(result.exit_code)
if __name__ == "__main__":
main()