390 lines
16 KiB
Python
390 lines
16 KiB
Python
#!/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()
|