#!/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()