+395
-20
@@ -13,6 +13,7 @@ from .diagram_renderer import diagram_engine_status
|
||||
from .diagram_schemes import (
|
||||
DiagramScheme,
|
||||
SCHEME_ID_RE,
|
||||
claim_user_scheme_if_diverged,
|
||||
ensure_user_schemes,
|
||||
get_schemes,
|
||||
load_schemes_from_path,
|
||||
@@ -20,7 +21,7 @@ from .diagram_schemes import (
|
||||
save_schemes,
|
||||
user_schemes_path,
|
||||
)
|
||||
from .dnd import enable_file_drop, first_markdown
|
||||
from .dnd import enable_file_drop, first_docx, first_markdown
|
||||
from .help_content import (
|
||||
SCHEMES_HELP,
|
||||
USAGE_HELP,
|
||||
@@ -200,6 +201,8 @@ class Md2GostApp:
|
||||
self._win_files: tk.Toplevel | None = None
|
||||
self._win_diagrams: tk.Toplevel | None = None
|
||||
self._win_schemes: tk.Toplevel | None = None
|
||||
self._win_student: tk.Toplevel | None = None
|
||||
self._win_meta: tk.Toplevel | None = None
|
||||
self._win_help: dict[str, tk.Toplevel] = {}
|
||||
|
||||
ensure_user_schemes()
|
||||
@@ -209,6 +212,7 @@ class Md2GostApp:
|
||||
self._build()
|
||||
self._apply_request(self.req)
|
||||
self.root.after(300, self._setup_dnd)
|
||||
self.root.after(400, self._ensure_student_profile)
|
||||
|
||||
if self.req.filename:
|
||||
self._set_markdown(self.req.filename, from_user_output=bool(self.req.output))
|
||||
@@ -229,6 +233,7 @@ class Md2GostApp:
|
||||
style.configure("Title.TLabel", font=font_title, foreground=NAVY, background=PAPER)
|
||||
style.configure("Hint.TLabel", foreground=MUTED, background=PAPER)
|
||||
style.configure("TCheckbutton", background=PAPER, font=font)
|
||||
style.configure("TRadiobutton", background=PAPER, foreground=INK, font=font)
|
||||
style.configure("TButton", font=font, padding=(10, 4))
|
||||
style.configure("TEntry", padding=3)
|
||||
style.configure("TCombobox", padding=3)
|
||||
@@ -242,6 +247,7 @@ class Md2GostApp:
|
||||
self.listing_var = tk.StringVar()
|
||||
self.emdash_var = tk.BooleanVar(value=False)
|
||||
self.hr_pagebreak_var = tk.BooleanVar(value=False)
|
||||
self.auto_title_var = tk.BooleanVar(value=False)
|
||||
self.open_var = tk.BooleanVar(value=True)
|
||||
self.check_var = tk.BooleanVar(value=False)
|
||||
self.check_only_var = tk.BooleanVar(value=False)
|
||||
@@ -268,6 +274,8 @@ class Md2GostApp:
|
||||
|
||||
settings = tk.Menu(menubar, tearoff=0)
|
||||
settings.add_command(label="Файлы…", command=self._open_files_window)
|
||||
settings.add_command(label="Студент…", command=self._open_student_window)
|
||||
settings.add_command(label="Метаданные…", command=self._open_metadata_window)
|
||||
settings.add_command(label="Диаграммы…", command=self._open_diagrams_window)
|
||||
menubar.add_cascade(label="Настройки", menu=settings)
|
||||
|
||||
@@ -340,6 +348,8 @@ class Md2GostApp:
|
||||
hint = ttk.Label(
|
||||
frame,
|
||||
text="Пустой шаблон — встроенный Template.docx. Титул и задание вставляются перед телом отчёта.\n"
|
||||
"Автотитул (info_conv.yaml / ```title) — галочка «Автотитул» на главном окне "
|
||||
"(по умолчанию выкл.; ФИО/группа — Настройки → Студент…).\n"
|
||||
"Стили JSON — опционально; также подхватывается md2gost.styles.json рядом с .md.",
|
||||
style="Hint.TLabel",
|
||||
wraplength=560,
|
||||
@@ -348,6 +358,225 @@ class Md2GostApp:
|
||||
win.protocol("WM_DELETE_WINDOW", lambda: self._close_toplevel(win, "_win_files"))
|
||||
return win
|
||||
|
||||
def _ensure_student_profile(self) -> None:
|
||||
from .user_profile import load_user_profile
|
||||
|
||||
if load_user_profile().is_complete():
|
||||
return
|
||||
self._open_student_window(first_run=True)
|
||||
|
||||
def _open_student_window(self, first_run: bool = False) -> None:
|
||||
existing = getattr(self, "_win_student", None)
|
||||
if existing is not None and existing.winfo_exists():
|
||||
existing.lift()
|
||||
existing.focus_force()
|
||||
return
|
||||
self._win_student = self._create_student_window(first_run=first_run)
|
||||
|
||||
def _create_student_window(self, *, first_run: bool = False) -> tk.Toplevel:
|
||||
from .user_profile import load_user_profile, save_user_profile
|
||||
|
||||
profile = load_user_profile()
|
||||
win = tk.Toplevel(self.root)
|
||||
win.title("Данные студента" if first_run else "Настройки — Студент")
|
||||
win.configure(bg=PAPER)
|
||||
win.minsize(420, 180)
|
||||
win.geometry("480x200")
|
||||
win.transient(self.root)
|
||||
if first_run:
|
||||
win.grab_set()
|
||||
frame = ttk.Frame(win, padding=12)
|
||||
frame.pack(fill=tk.BOTH, expand=True)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
|
||||
ttk.Label(
|
||||
frame,
|
||||
text="ФИО и группа нужны для автотитульника (info_conv.yaml / ```title).",
|
||||
style="Hint.TLabel",
|
||||
wraplength=440,
|
||||
).grid(row=0, column=0, columnspan=2, sticky="w", pady=(0, 8))
|
||||
|
||||
student_var = tk.StringVar(value=profile.student)
|
||||
group_var = tk.StringVar(value=profile.group)
|
||||
ttk.Label(frame, text="ФИО").grid(row=1, column=0, sticky="w", pady=4)
|
||||
ttk.Entry(frame, textvariable=student_var).grid(row=1, column=1, sticky="ew", pady=4, padx=(8, 0))
|
||||
ttk.Label(frame, text="Группа").grid(row=2, column=0, sticky="w", pady=4)
|
||||
ttk.Entry(frame, textvariable=group_var).grid(row=2, column=1, sticky="ew", pady=4, padx=(8, 0))
|
||||
|
||||
def _save(close: bool = True) -> None:
|
||||
student = student_var.get().strip()
|
||||
group = group_var.get().strip()
|
||||
if not student or not group:
|
||||
messagebox.showwarning(
|
||||
"Данные студента",
|
||||
"Заполните ФИО и группу.",
|
||||
parent=win,
|
||||
)
|
||||
return
|
||||
current = load_user_profile()
|
||||
current.student = student
|
||||
current.group = group
|
||||
save_user_profile(current)
|
||||
if close:
|
||||
self._close_toplevel(win, "_win_student")
|
||||
|
||||
btns = ttk.Frame(frame)
|
||||
btns.grid(row=3, column=0, columnspan=2, sticky="e", pady=(12, 0))
|
||||
ttk.Button(btns, text="Сохранить", command=lambda: _save(True)).pack(side=tk.RIGHT)
|
||||
|
||||
def _on_close() -> None:
|
||||
if first_run and not load_user_profile().is_complete():
|
||||
# Allow closing only after save; remind once
|
||||
if not student_var.get().strip() or not group_var.get().strip():
|
||||
messagebox.showinfo(
|
||||
"Данные студента",
|
||||
"Можно заполнить позже: Настройки → Студент…\n"
|
||||
"Без ФИО/группы автотитул не соберётся.",
|
||||
parent=win,
|
||||
)
|
||||
self._close_toplevel(win, "_win_student")
|
||||
|
||||
win.protocol("WM_DELETE_WINDOW", _on_close)
|
||||
return win
|
||||
|
||||
def _open_metadata_window(self) -> None:
|
||||
self._win_meta = self._raise_or_create(self._win_meta, self._create_metadata_window)
|
||||
|
||||
def _create_metadata_window(self) -> tk.Toplevel:
|
||||
from .doc_metadata import (
|
||||
AUTHOR_SOURCES,
|
||||
DEFAULT_AUTHOR_SOURCE,
|
||||
DEFAULT_DOC_COMMENTS,
|
||||
DocumentMetadata,
|
||||
os_user,
|
||||
)
|
||||
from .user_profile import load_user_profile, save_user_profile
|
||||
|
||||
profile = load_user_profile()
|
||||
meta = profile.metadata or DocumentMetadata()
|
||||
win = tk.Toplevel(self.root)
|
||||
win.title("Настройки — Метаданные")
|
||||
win.configure(bg=PAPER)
|
||||
win.minsize(520, 420)
|
||||
win.geometry("580x500")
|
||||
win.transient(self.root)
|
||||
frame = ttk.Frame(win, padding=12)
|
||||
frame.pack(fill=tk.BOTH, expand=True)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
|
||||
ttk.Label(
|
||||
frame,
|
||||
text=(
|
||||
"Свойства файла Word (Файл → сведения): автор, название, тема, теги, примечание. "
|
||||
"Хранятся в md2gost.user.json рядом с приложением."
|
||||
),
|
||||
style="Hint.TLabel",
|
||||
wraplength=540,
|
||||
).grid(row=0, column=0, columnspan=2, sticky="w", pady=(0, 8))
|
||||
|
||||
src_value = meta.author_source if meta.author_source in AUTHOR_SOURCES else DEFAULT_AUTHOR_SOURCE
|
||||
author_src = tk.StringVar(value=src_value)
|
||||
author_var = tk.StringVar(value=meta.author)
|
||||
last_mod_var = tk.StringVar(value=meta.last_modified_by)
|
||||
title_var = tk.StringVar(value=meta.title)
|
||||
subject_var = tk.StringVar(value=meta.subject)
|
||||
keywords_var = tk.StringVar(value=meta.keywords)
|
||||
category_var = tk.StringVar(value=meta.category)
|
||||
comments_var = tk.StringVar(value=meta.comments)
|
||||
|
||||
ttk.Label(frame, text="Автор").grid(row=1, column=0, sticky="nw", pady=(4, 0))
|
||||
src_box = ttk.Frame(frame)
|
||||
src_box.grid(row=1, column=1, sticky="ew", pady=(4, 0), padx=(8, 0))
|
||||
os_name = os_user() or "—"
|
||||
student_name = (profile.student or "").strip()
|
||||
student_text = (
|
||||
f"ФИО студента ({student_name})"
|
||||
if student_name
|
||||
else "ФИО студента (сначала Настройки → Студент…)"
|
||||
)
|
||||
ttk.Radiobutton(
|
||||
src_box, text=f"Имя пользователя Windows ({os_name})",
|
||||
variable=author_src, value="os",
|
||||
).pack(anchor="w")
|
||||
ttk.Radiobutton(
|
||||
src_box, text=student_text, variable=author_src, value="student",
|
||||
).pack(anchor="w")
|
||||
ttk.Radiobutton(
|
||||
src_box, text="Указать вручную", variable=author_src, value="custom",
|
||||
).pack(anchor="w")
|
||||
|
||||
author_entry = ttk.Entry(frame, textvariable=author_var)
|
||||
author_entry.grid(row=2, column=1, sticky="ew", pady=(2, 4), padx=(8, 0))
|
||||
_tip(author_entry, "Используется, если выбран пункт «Указать вручную»")
|
||||
|
||||
def _sync_author_entry(*_a) -> None:
|
||||
state = "normal" if author_src.get() == "custom" else "disabled"
|
||||
try:
|
||||
author_entry.configure(state=state)
|
||||
except tk.TclError:
|
||||
pass
|
||||
|
||||
author_src.trace_add("write", _sync_author_entry)
|
||||
_sync_author_entry()
|
||||
|
||||
def _row(row: int, label: str, var: tk.StringVar, tip: str) -> None:
|
||||
ttk.Label(frame, text=label).grid(row=row, column=0, sticky="w", pady=4)
|
||||
entry = ttk.Entry(frame, textvariable=var)
|
||||
entry.grid(row=row, column=1, sticky="ew", pady=4, padx=(8, 0))
|
||||
_tip(entry, tip)
|
||||
|
||||
_row(3, "Кем изменён", last_mod_var, "Пусто — то же значение, что у автора")
|
||||
_row(4, "Название", title_var, "dc:title в свойствах файла; пусто — не задавать")
|
||||
_row(5, "Тема", subject_var, "Тема документа в свойствах файла")
|
||||
_row(6, "Ключевые слова", keywords_var, "Теги в свойствах файла Word")
|
||||
_row(7, "Категория", category_var, "Категория в свойствах файла")
|
||||
_row(
|
||||
8,
|
||||
"Примечание",
|
||||
comments_var,
|
||||
f"По умолчанию: «{DEFAULT_DOC_COMMENTS}». Пусто — не писать примечание.",
|
||||
)
|
||||
ttk.Label(
|
||||
frame,
|
||||
text="Пустое примечание убирает и текст md2gost, и «generated by python-docx» из шаблона.",
|
||||
style="Hint.TLabel",
|
||||
wraplength=540,
|
||||
).grid(row=9, column=0, columnspan=2, sticky="w", pady=(4, 0))
|
||||
|
||||
def _save() -> None:
|
||||
src = author_src.get() if author_src.get() in AUTHOR_SOURCES else DEFAULT_AUTHOR_SOURCE
|
||||
current = load_user_profile()
|
||||
current.metadata = DocumentMetadata(
|
||||
author_source=src,
|
||||
author=author_var.get().strip(),
|
||||
last_modified_by=last_mod_var.get().strip(),
|
||||
title=title_var.get().strip(),
|
||||
subject=subject_var.get().strip(),
|
||||
keywords=keywords_var.get().strip(),
|
||||
comments=comments_var.get().strip(),
|
||||
category=category_var.get().strip(),
|
||||
)
|
||||
save_user_profile(current)
|
||||
self._close_toplevel(win, "_win_meta")
|
||||
|
||||
def _reset() -> None:
|
||||
author_src.set(DEFAULT_AUTHOR_SOURCE)
|
||||
author_var.set("")
|
||||
last_mod_var.set("")
|
||||
title_var.set("")
|
||||
subject_var.set("")
|
||||
keywords_var.set("")
|
||||
category_var.set("")
|
||||
comments_var.set(DEFAULT_DOC_COMMENTS)
|
||||
|
||||
btns = ttk.Frame(frame)
|
||||
btns.grid(row=10, column=0, columnspan=2, sticky="e", pady=(12, 0))
|
||||
ttk.Button(btns, text="Сбросить", command=_reset).pack(side=tk.LEFT, padx=(0, 8))
|
||||
ttk.Button(btns, text="Сохранить", command=_save).pack(side=tk.RIGHT)
|
||||
|
||||
win.protocol("WM_DELETE_WINDOW", lambda: self._close_toplevel(win, "_win_meta"))
|
||||
return win
|
||||
|
||||
def _create_diagrams_window(self) -> tk.Toplevel:
|
||||
win = tk.Toplevel(self.root)
|
||||
win.title("Настройки — Диаграммы")
|
||||
@@ -362,10 +591,10 @@ class Md2GostApp:
|
||||
hint = ttk.Label(
|
||||
extra,
|
||||
text=(
|
||||
"Блоки ```uml / ```uml-c4 / ```bpmn / ```mermaid сами станут рисунками.\n"
|
||||
"Блоки ```uml / ```uml-c4 / ```mermaid / ```idef0 / ```dfd сами станут рисунками.\n"
|
||||
"Схемы редактируются в меню «Шаблоны UML» (файл md2gost.schemes.json).\n"
|
||||
"UML: PlantUML + Java, иначе Kroki. Mermaid: системный браузер → "
|
||||
"Chromium → QuickJS → Kroki (kroki.io остаётся запасным)."
|
||||
"Chromium → QuickJS → Kroki. IDEF0 — Pillow. DFD — Graphviz / Kroki."
|
||||
),
|
||||
style="Hint.TLabel",
|
||||
wraplength=640,
|
||||
@@ -377,14 +606,17 @@ 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", rowspan=2)
|
||||
self.diag_status.grid(row=0, column=0, sticky="w", rowspan=3)
|
||||
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_gv = ttk.Button(status_row, text="Скачать Graphviz", command=self._download_graphviz)
|
||||
btn_gv.grid(row=1, column=1, sticky="e", padx=(8, 0), pady=(4, 0))
|
||||
_tip(btn_gv, "Скачать portable Graphviz (dot/neato) для локальных DFD")
|
||||
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))
|
||||
btn_cr.grid(row=2, column=1, sticky="e", padx=(8, 0), pady=(4, 0))
|
||||
_tip(
|
||||
btn_cr,
|
||||
"Скачать Chromium для Mermaid в %LOCALAPPDATA%\\md2gost "
|
||||
@@ -629,7 +861,7 @@ class Md2GostApp:
|
||||
def _build_schemes_help(self, parent) -> None:
|
||||
ttk.Label(
|
||||
parent,
|
||||
text="Настраиваемые схемы диаграмм (uml-c4, bpmn, …)",
|
||||
text="Настраиваемые схемы диаграмм (uml-c4, usecase, …)",
|
||||
style="Hint.TLabel",
|
||||
).pack(anchor=tk.W, pady=(0, 6))
|
||||
box = self._readonly_text(parent, SCHEMES_HELP.strip() + "\n")
|
||||
@@ -641,7 +873,7 @@ class Md2GostApp:
|
||||
parent,
|
||||
text="Скопируйте промпт и вставьте в ChatGPT / Cursor / Copilot. "
|
||||
"Кнопки схем дописывают макросы из md2gost.schemes.json — "
|
||||
"когда ИИ должен знать C4/BPMN/свои шаблоны. Следующим сообщением — тип, тема, черновик.",
|
||||
"когда ИИ должен знать C4/свои шаблоны. Следующим сообщением — тип, тема, черновик.",
|
||||
style="Hint.TLabel",
|
||||
wraplength=640,
|
||||
).pack(anchor=tk.W, pady=(0, 8))
|
||||
@@ -800,7 +1032,7 @@ class Md2GostApp:
|
||||
self.zone.pack(fill=tk.X, pady=(0, 8))
|
||||
self.zone_label = tk.Label(
|
||||
self.zone,
|
||||
text="Перетащите .md сюда · или нажмите, чтобы выбрать файл",
|
||||
text="Перетащите .md или .docx сюда · или нажмите, чтобы выбрать файл",
|
||||
bg=ZONE, fg=NAVY, font=("Segoe UI", 11) if os.name == "nt" else ("Helvetica", 11),
|
||||
justify=tk.CENTER, pady=14,
|
||||
)
|
||||
@@ -812,8 +1044,8 @@ class Md2GostApp:
|
||||
self.zone_path.pack(fill=tk.X, pady=(0, 8))
|
||||
for w in (self.zone, self.zone_label, self.zone_path):
|
||||
w.bind("<Button-1>", lambda _e: self._browse_md())
|
||||
_tip(self.zone, "Перетащите .md или кликните, чтобы выбрать файл")
|
||||
_tip(self.zone_label, "Перетащите .md или кликните, чтобы выбрать файл")
|
||||
_tip(self.zone, "Перетащите .md / .docx или кликните, чтобы выбрать файл")
|
||||
_tip(self.zone_label, "Перетащите .md / .docx или кликните, чтобы выбрать файл")
|
||||
|
||||
grid = ttk.Frame(outer)
|
||||
grid.pack(fill=tk.X)
|
||||
@@ -857,6 +1089,11 @@ class Md2GostApp:
|
||||
checks = [
|
||||
("Заменять «—» на «-»", self.emdash_var, "Типографское тире → дефис (методичка обычно требует «—»)"),
|
||||
("--- → разрыв страницы", self.hr_pagebreak_var, "Строка --- станет page break в Word"),
|
||||
(
|
||||
"Автотитул",
|
||||
self.auto_title_var,
|
||||
"Собрать титул из info_conv.yaml / ```title (нужны ФИО и группа). По умолчанию выкл.",
|
||||
),
|
||||
("Открыть после конвертации", self.open_var, "Открыть DOCX после успешной сборки"),
|
||||
("Проверить по ТЗ", self.check_var, "Замечания по структуре, «рис.», источникам"),
|
||||
("Только проверка", self.check_only_var, "Проверить без записи DOCX"),
|
||||
@@ -886,7 +1123,17 @@ class Md2GostApp:
|
||||
relief=tk.FLAT, padx=22, pady=7, cursor="hand2",
|
||||
)
|
||||
self.convert_btn.pack(side=tk.LEFT)
|
||||
_tip(self.convert_btn, "Собрать DOCX из выбранного markdown")
|
||||
_tip(self.convert_btn, "Собрать DOCX из markdown или импортировать DOCX → Markdown")
|
||||
self.import_btn = tk.Button(
|
||||
actions, text="Импорт DOCX→MD",
|
||||
command=self._import_docx,
|
||||
bg="#3d5a40", fg="white", activebackground="#2d4430", activeforeground="white",
|
||||
disabledforeground="#d0d5dc",
|
||||
font=("Segoe UI", 11) if os.name == "nt" else ("Helvetica", 11),
|
||||
relief=tk.FLAT, padx=14, pady=7, cursor="hand2",
|
||||
)
|
||||
self.import_btn.pack(side=tk.LEFT, padx=(8, 0))
|
||||
_tip(self.import_btn, "Выбрать .docx и сохранить диалект md2gost (.md + медиа)")
|
||||
self.progress = ttk.Progressbar(actions, mode="indeterminate", length=180)
|
||||
self.progress.pack(side=tk.LEFT, padx=8, fill=tk.X, expand=True)
|
||||
|
||||
@@ -901,7 +1148,7 @@ class Md2GostApp:
|
||||
self.log.configure(yscrollcommand=scroll.set)
|
||||
self.log.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
scroll.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
self.log.insert("1.0", "Готово к работе. Перетащите markdown-файл в верхнюю область.\n")
|
||||
self.log.insert("1.0", "Готово к работе. Перетащите .md (сборка DOCX) или .docx (импорт в Markdown).\n")
|
||||
self.log.configure(state=tk.DISABLED)
|
||||
|
||||
def _build_schemes_tab(self, parent) -> None:
|
||||
@@ -1067,6 +1314,7 @@ class Md2GostApp:
|
||||
scheme = self._scheme_from_form(old_id)
|
||||
if scheme is None:
|
||||
return None
|
||||
scheme = claim_user_scheme_if_diverged(scheme)
|
||||
if old_id and old_id != scheme.id and old_id in self._schemes:
|
||||
del self._schemes[old_id]
|
||||
self._schemes[scheme.id] = scheme
|
||||
@@ -1197,7 +1445,7 @@ class Md2GostApp:
|
||||
if backend == "none":
|
||||
self._append_log("Перетаскивание недоступно — выберите файл кликом по области.")
|
||||
else:
|
||||
self._append_log("Можно перетаскивать .md из проводника в окно.")
|
||||
self._append_log("Можно перетаскивать .md / .docx из проводника в окно.")
|
||||
|
||||
def _on_drop(self, paths: list[str]) -> None:
|
||||
md = first_markdown(paths)
|
||||
@@ -1205,8 +1453,16 @@ class Md2GostApp:
|
||||
self._set_markdown(md)
|
||||
self._flash_zone(ZONE_OK)
|
||||
return
|
||||
docx = first_docx(paths)
|
||||
if docx:
|
||||
self._set_docx(docx)
|
||||
self._flash_zone(ZONE_OK)
|
||||
return
|
||||
if paths:
|
||||
self._append_log("Нужен файл .md, а не: " + ", ".join(os.path.basename(p) for p in paths))
|
||||
self._append_log(
|
||||
"Нужен файл .md или .docx, а не: "
|
||||
+ ", ".join(os.path.basename(p) for p in paths)
|
||||
)
|
||||
self._flash_zone("#f8d7da")
|
||||
|
||||
def _flash_zone(self, color: str) -> None:
|
||||
@@ -1222,10 +1478,19 @@ class Md2GostApp:
|
||||
|
||||
def _browse_md(self) -> None:
|
||||
path = filedialog.askopenfilename(
|
||||
title="Исходный Markdown",
|
||||
filetypes=[("Markdown", "*.md"), ("Все файлы", "*.*")],
|
||||
title="Исходный Markdown или Word",
|
||||
filetypes=[
|
||||
("Markdown / Word", "*.md;*.docx"),
|
||||
("Markdown", "*.md"),
|
||||
("Word", "*.docx"),
|
||||
("Все файлы", "*.*"),
|
||||
],
|
||||
)
|
||||
if path:
|
||||
if not path:
|
||||
return
|
||||
if path.lower().endswith(".docx"):
|
||||
self._set_docx(path)
|
||||
else:
|
||||
self._set_markdown(path)
|
||||
|
||||
def _browse_output(self) -> None:
|
||||
@@ -1290,6 +1555,55 @@ class Md2GostApp:
|
||||
"Можно ничего не делать — диаграммы нарисует kroki.io.",
|
||||
)
|
||||
|
||||
def _download_graphviz(self) -> None:
|
||||
from .dfd import (
|
||||
cached_graphviz_root,
|
||||
fetch_graphviz,
|
||||
vendor_graphviz_root,
|
||||
)
|
||||
|
||||
dest = vendor_graphviz_root()
|
||||
try:
|
||||
dest.mkdir(parents=True, exist_ok=True)
|
||||
probe = dest / ".write_test"
|
||||
probe.write_text("ok", encoding="utf-8")
|
||||
probe.unlink()
|
||||
except OSError:
|
||||
dest = cached_graphviz_root()
|
||||
self._append_log(f"Скачиваю Graphviz → {dest}")
|
||||
self.root.update_idletasks()
|
||||
|
||||
def work() -> None:
|
||||
ok = fetch_graphviz(dest)
|
||||
self.root.after(0, lambda: self._on_graphviz_installed(ok, dest))
|
||||
|
||||
import threading
|
||||
|
||||
threading.Thread(target=work, daemon=True).start()
|
||||
|
||||
def _on_graphviz_installed(self, ok: bool, dest) -> None:
|
||||
self._refresh_diagram_status()
|
||||
if ok:
|
||||
engine = None
|
||||
try:
|
||||
from .dfd import resolve_graphviz_engine
|
||||
|
||||
engine = resolve_graphviz_engine(False)
|
||||
except Exception:
|
||||
pass
|
||||
messagebox.showinfo(
|
||||
"md2gost",
|
||||
f"Graphviz сохранён:\n{dest}\n\n"
|
||||
f"Движок: {engine or 'dot в каталоге'}",
|
||||
)
|
||||
else:
|
||||
messagebox.showerror(
|
||||
"md2gost",
|
||||
"Не удалось скачать Graphviz.\n"
|
||||
"DFD может пойти через kroki.io "
|
||||
"(«Как рисовать» → через интернет) или системный PATH.",
|
||||
)
|
||||
|
||||
def _download_chromium(self) -> None:
|
||||
from .mermaid_renderer import install_playwright_chromium, playwright_browsers_dir, playwright_available
|
||||
|
||||
@@ -1351,6 +1665,32 @@ class Md2GostApp:
|
||||
self._auto_output = True
|
||||
self._append_log(f"Файл: {path}")
|
||||
|
||||
def _set_docx(self, path: str) -> None:
|
||||
path = os.path.abspath(path)
|
||||
self._last_md = path
|
||||
self.zone_label.configure(text=os.path.basename(path))
|
||||
self.zone_path.configure(text=path)
|
||||
if self._auto_output:
|
||||
stem = os.path.splitext(path)[0]
|
||||
self.output_var.set(stem + ".md")
|
||||
self._auto_output = True
|
||||
self._append_log(f"DOCX для импорта: {path}")
|
||||
|
||||
def _import_docx(self) -> None:
|
||||
"""Pick a DOCX (or use current) and run word2md."""
|
||||
if self._busy:
|
||||
return
|
||||
path = self._last_md if self._last_md.lower().endswith(".docx") else ""
|
||||
if not path:
|
||||
path = filedialog.askopenfilename(
|
||||
title="Импорт DOCX → Markdown",
|
||||
filetypes=[("Word", "*.docx"), ("Все файлы", "*.*")],
|
||||
)
|
||||
if not path:
|
||||
return
|
||||
self._set_docx(path)
|
||||
self._run_convert(force_import=True)
|
||||
|
||||
def _apply_request(self, req: ConvertRequest) -> None:
|
||||
self.type_var.set(_display(DOC_TYPE_LABELS, req.doc_type if req.doc_type in DOC_TYPES else "practice"))
|
||||
self.heading_var.set(_display(HEADING_LABELS, req.heading_numbering or DEFAULT_HEADING_NUMBERING))
|
||||
@@ -1363,6 +1703,7 @@ class Md2GostApp:
|
||||
self.scale_var.set(str(scale).rstrip("0").rstrip(".") if isinstance(scale, float) else str(scale))
|
||||
self.emdash_var.set(bool(req.emdash_to_hyphen))
|
||||
self.hr_pagebreak_var.set(bool(req.hr_pagebreak))
|
||||
self.auto_title_var.set(bool(getattr(req, "auto_title", False)))
|
||||
self.open_var.set(True)
|
||||
self.check_var.set(bool(req.check))
|
||||
self.check_only_var.set(bool(req.check_only))
|
||||
@@ -1398,6 +1739,7 @@ class Md2GostApp:
|
||||
listing_continuation=_combo_key(self.listing_var.get(), LISTING_CONTINUATION_MODES, DEFAULT_LISTING_CONTINUATION),
|
||||
emdash_to_hyphen=self.emdash_var.get(),
|
||||
hr_pagebreak=self.hr_pagebreak_var.get(),
|
||||
auto_title=self.auto_title_var.get(),
|
||||
title=self.title_var.get().strip() or None,
|
||||
assignment=self.assignment_var.get().strip() or None,
|
||||
check=self.check_var.get() or self.check_only_var.get(),
|
||||
@@ -1436,13 +1778,39 @@ class Md2GostApp:
|
||||
return value if value >= 1 else 1.0
|
||||
|
||||
def _convert(self) -> None:
|
||||
self._run_convert(force_import=False)
|
||||
|
||||
def _run_convert(self, *, force_import: bool) -> None:
|
||||
if self._busy:
|
||||
return
|
||||
req = self._collect()
|
||||
if not req.filename:
|
||||
messagebox.showinfo("md2gost", "Сначала перетащите или выберите markdown-файл.")
|
||||
messagebox.showinfo(
|
||||
"md2gost",
|
||||
"Сначала перетащите или выберите .md (сборка) или .docx (импорт).",
|
||||
)
|
||||
return
|
||||
if not req.check_only:
|
||||
is_docx = req.filename.lower().endswith(".docx")
|
||||
if force_import and not is_docx:
|
||||
messagebox.showinfo("md2gost", "Для импорта нужен файл .docx.")
|
||||
return
|
||||
if is_docx:
|
||||
target = (req.output or "").strip()
|
||||
if target.lower().endswith(".docx"):
|
||||
target = os.path.splitext(req.filename)[0] + ".md"
|
||||
req.output = target
|
||||
self.output_var.set(target)
|
||||
if target and os.path.isfile(target):
|
||||
if not messagebox.askyesno(
|
||||
"md2gost", f"Файл уже есть:\n{target}\n\nПерезаписать?"
|
||||
):
|
||||
base, ext = os.path.splitext(target)
|
||||
target = f"{base}_import{ext or '.md'}"
|
||||
self._append_log(f"Не перезаписываем, пишем в {target}")
|
||||
req.output = target
|
||||
self.output_var.set(target)
|
||||
self._append_log("Импорт DOCX → Markdown…")
|
||||
elif not req.check_only:
|
||||
target = (req.output or "").strip() or (
|
||||
default_output_path(req.filename) if req.filename else ""
|
||||
)
|
||||
@@ -1455,10 +1823,15 @@ class Md2GostApp:
|
||||
req.output = target
|
||||
self.output_var.set(target)
|
||||
self._auto_output = False
|
||||
self._append_log("Конвертация…")
|
||||
else:
|
||||
self._append_log("Проверка…")
|
||||
|
||||
self._busy = True
|
||||
self.convert_btn.configure(state=tk.DISABLED, bg=NAVY_DIM)
|
||||
if hasattr(self, "import_btn"):
|
||||
self.import_btn.configure(state=tk.DISABLED)
|
||||
self.progress.start(12)
|
||||
self._append_log("Конвертация…")
|
||||
|
||||
def work():
|
||||
result = convert(req, log=lambda m: self.root.after(0, self._append_log, m))
|
||||
@@ -1469,6 +1842,8 @@ class Md2GostApp:
|
||||
def _on_done(self, result) -> None:
|
||||
self._busy = False
|
||||
self.convert_btn.configure(state=tk.NORMAL, bg=NAVY)
|
||||
if hasattr(self, "import_btn"):
|
||||
self.import_btn.configure(state=tk.NORMAL, bg="#3d5a40")
|
||||
self.progress.stop()
|
||||
if self._debug_next or self.debug_var.get():
|
||||
self.debug_var.set(False)
|
||||
|
||||
Reference in New Issue
Block a user