- update документация - промт для ии полу конфигурируемый
This commit is contained in:
+3
-1
@@ -2,6 +2,8 @@
|
||||
|
||||
Конвертер Markdown → DOCX по методическим указаниям РТУ МИРЭА (ГОСТ 7.32-2017) на базе [md2gost](https://github.com/benzlokzik/md2gost).
|
||||
|
||||
**Полная документация:** [`docs/`](../docs/) (быстрый старт, синтаксис, схемы, типы, CLI/GUI, промпты).
|
||||
|
||||
## Установка
|
||||
|
||||
```bash
|
||||
@@ -23,7 +25,7 @@ md2gost-gui
|
||||
|
||||
На Windows файл можно бросить из Проводника в верхнюю область окна. Клик по области — выбор через диалог. Все флаги CLI есть в форме (тип, нумерация, TOC, тире, `---` → разрыв страницы, титул/задание, диаграммы, проверка ТЗ).
|
||||
|
||||
Вкладки **Инструкция** и **Промпт для ИИ** — справка и копирование системного промпта (МИРЭА / ПИС) в буфер.
|
||||
Вкладки **Инструкция** и **Промпт для ИИ** — справка и копирование системного промпта (с опциональным дописыванием UML-схем) в буфер.
|
||||
|
||||
### Сборка exe (Windows)
|
||||
|
||||
|
||||
@@ -166,6 +166,14 @@ def build_parser() -> ArgumentParser:
|
||||
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
|
||||
@@ -197,6 +205,7 @@ def request_from_args(args) -> ConvertRequest:
|
||||
diagram_format=args.diagram_format,
|
||||
diagram_scale=float(args.diagram_scale),
|
||||
schemes_path=args.schemes_path,
|
||||
styles_path=getattr(args, "styles_path", None),
|
||||
debug=args.debug,
|
||||
open_when_done=bool(args.debug),
|
||||
)
|
||||
|
||||
+16
-2
@@ -1,3 +1,5 @@
|
||||
import os
|
||||
|
||||
import docx
|
||||
from docx.document import Document
|
||||
|
||||
@@ -43,7 +45,8 @@ class Converter:
|
||||
toc_mode: str = DEFAULT_TOC_MODE,
|
||||
table_continuation: str = DEFAULT_TABLE_CONTINUATION,
|
||||
listing_continuation: str = DEFAULT_LISTING_CONTINUATION,
|
||||
hr_pagebreak: bool = False):
|
||||
hr_pagebreak: bool = False,
|
||||
styles_path: str | None = None):
|
||||
if heading_numbering not in HEADING_NUMBERING_MODES:
|
||||
raise ValueError(
|
||||
f"heading_numbering must be one of {HEADING_NUMBERING_MODES}, "
|
||||
@@ -71,10 +74,17 @@ class Converter:
|
||||
self._table_continuation = table_continuation
|
||||
self._listing_continuation = listing_continuation
|
||||
self._hr_pagebreak = hr_pagebreak
|
||||
self._styles_path = styles_path
|
||||
self._profile = get_profile(doc_type)
|
||||
self._document: Document = docx.Document(template_path)
|
||||
self._document._body.clear_content()
|
||||
apply_document_styles(self._document, self._profile.style_preset)
|
||||
md_dir = os.path.dirname(os.path.abspath(input_path)) or "."
|
||||
self._style_config = apply_document_styles(
|
||||
self._document,
|
||||
self._profile.style_preset,
|
||||
md_dir=md_dir,
|
||||
styles_path=styles_path,
|
||||
)
|
||||
self._debugger = Debugger(self._document) if debug else None
|
||||
with open(input_path, encoding="utf-8") as f:
|
||||
raw = f.read()
|
||||
@@ -123,6 +133,10 @@ class Converter:
|
||||
def document(self) -> Document:
|
||||
return self._document
|
||||
|
||||
@property
|
||||
def style_config(self):
|
||||
return self._style_config
|
||||
|
||||
@property
|
||||
def raw_markdown(self) -> str:
|
||||
return self._raw_markdown
|
||||
|
||||
+175
-15
@@ -14,13 +14,20 @@ from .diagram_schemes import (
|
||||
DiagramScheme,
|
||||
SCHEME_ID_RE,
|
||||
ensure_user_schemes,
|
||||
get_schemes,
|
||||
load_schemes_from_path,
|
||||
reload_schemes,
|
||||
save_schemes,
|
||||
user_schemes_path,
|
||||
)
|
||||
from .dnd import enable_file_drop, first_markdown
|
||||
from .help_content import SCHEMES_HELP, USAGE_HELP, load_prompt_catalog
|
||||
from .help_content import (
|
||||
SCHEMES_HELP,
|
||||
USAGE_HELP,
|
||||
compose_prompt,
|
||||
load_docs_catalog,
|
||||
load_prompt_catalog,
|
||||
)
|
||||
from .pipeline import ConvertRequest, convert, default_output_path, timestamped_output_path
|
||||
from .profiles import (
|
||||
DEFAULT_HEADING_NUMBERING,
|
||||
@@ -244,6 +251,7 @@ class Md2GostApp:
|
||||
self.template_var = tk.StringVar()
|
||||
self.title_var = tk.StringVar()
|
||||
self.assignment_var = tk.StringVar()
|
||||
self.styles_var = tk.StringVar()
|
||||
self.fallback_var = tk.StringVar()
|
||||
self.format_var = tk.StringVar()
|
||||
self.plantuml_var = tk.StringVar()
|
||||
@@ -266,6 +274,7 @@ class Md2GostApp:
|
||||
|
||||
help_menu = tk.Menu(menubar, tearoff=0)
|
||||
help_menu.add_command(label="Инструкция", command=lambda: self._open_help_window("usage"))
|
||||
help_menu.add_command(label="Документация", command=lambda: self._open_help_window("docs"))
|
||||
help_menu.add_command(label="Схемы", command=lambda: self._open_help_window("schemes"))
|
||||
help_menu.add_command(label="Промпт для ИИ", command=lambda: self._open_help_window("prompts"))
|
||||
menubar.add_cascade(label="Справка", menu=help_menu)
|
||||
@@ -313,8 +322,8 @@ class Md2GostApp:
|
||||
win = tk.Toplevel(self.root)
|
||||
win.title("Настройки — Файлы")
|
||||
win.configure(bg=PAPER)
|
||||
win.minsize(520, 200)
|
||||
win.geometry("620x240")
|
||||
win.minsize(520, 240)
|
||||
win.geometry("620x280")
|
||||
win.transient(self.root)
|
||||
frame = ttk.Frame(win, padding=12)
|
||||
frame.pack(fill=tk.BOTH, expand=True)
|
||||
@@ -325,13 +334,16 @@ class Md2GostApp:
|
||||
tip="DOCX титула, вставляется перед телом отчёта")
|
||||
self._row_path(frame, 2, "Бланк задания", self.assignment_var, self._browse_assignment, clearable=True,
|
||||
tip="DOCX бланка задания, вставляется перед телом отчёта")
|
||||
self._row_path(frame, 3, "Стили JSON", self.styles_var, self._browse_styles, clearable=True,
|
||||
tip="Оверлей md2gost.styles.json поверх пресета типа документа")
|
||||
hint = ttk.Label(
|
||||
frame,
|
||||
text="Пустой шаблон — встроенный Template.docx. Титул и задание вставляются перед телом отчёта.",
|
||||
text="Пустой шаблон — встроенный Template.docx. Титул и задание вставляются перед телом отчёта.\n"
|
||||
"Стили JSON — опционально; также подхватывается md2gost.styles.json рядом с .md.",
|
||||
style="Hint.TLabel",
|
||||
wraplength=560,
|
||||
)
|
||||
hint.grid(row=3, column=0, columnspan=2, sticky="w", pady=(8, 0))
|
||||
hint.grid(row=4, column=0, columnspan=2, sticky="w", pady=(8, 0))
|
||||
win.protocol("WM_DELETE_WINDOW", lambda: self._close_toplevel(win, "_win_files"))
|
||||
return win
|
||||
|
||||
@@ -456,6 +468,7 @@ class Md2GostApp:
|
||||
def _create_help_window(self, kind: str) -> tk.Toplevel:
|
||||
titles = {
|
||||
"usage": "Справка — Инструкция",
|
||||
"docs": "Справка — Документация",
|
||||
"schemes": "Справка — Схемы",
|
||||
"prompts": "Справка — Промпт для ИИ",
|
||||
}
|
||||
@@ -463,12 +476,17 @@ class Md2GostApp:
|
||||
win.title(titles.get(kind, "Справка"))
|
||||
win.configure(bg=PAPER)
|
||||
win.minsize(560, 400)
|
||||
win.geometry("700x560")
|
||||
if kind == "docs":
|
||||
win.geometry("780x560")
|
||||
else:
|
||||
win.geometry("700x560")
|
||||
win.transient(self.root)
|
||||
frame = ttk.Frame(win, padding=12)
|
||||
frame.pack(fill=tk.BOTH, expand=True)
|
||||
if kind == "usage":
|
||||
self._build_help(frame)
|
||||
elif kind == "docs":
|
||||
self._build_docs_browser(frame)
|
||||
elif kind == "schemes":
|
||||
self._build_schemes_help(frame)
|
||||
else:
|
||||
@@ -540,6 +558,60 @@ class Md2GostApp:
|
||||
box = self._readonly_text(parent, USAGE_HELP.strip() + "\n")
|
||||
box.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
def _build_docs_browser(self, parent) -> None:
|
||||
ttk.Label(
|
||||
parent,
|
||||
text="Страницы слева; ссылки вида (file.md) в тексте открывайте тем же списком. "
|
||||
"Markdown показывается как есть.",
|
||||
style="Hint.TLabel",
|
||||
wraplength=740,
|
||||
).pack(anchor=tk.W, pady=(0, 8))
|
||||
|
||||
catalog = load_docs_catalog()
|
||||
if not catalog:
|
||||
ttk.Label(
|
||||
parent,
|
||||
text="Документация не найдена (папка docs/ рядом с программой или внутри сборки).",
|
||||
style="Hint.TLabel",
|
||||
).pack(anchor=tk.W)
|
||||
return
|
||||
|
||||
self._docs_catalog = catalog
|
||||
body = ttk.Frame(parent)
|
||||
body.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
left = ttk.Frame(body)
|
||||
left.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10))
|
||||
self._docs_list = tk.Listbox(left, height=22, width=28, exportselection=False)
|
||||
self._docs_list.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
|
||||
for _name, title, _text in catalog:
|
||||
self._docs_list.insert(tk.END, title)
|
||||
self._docs_list.bind("<<ListboxSelect>>", self._on_docs_select)
|
||||
_tip(self._docs_list, "Разделы документации (docs/*.md)")
|
||||
|
||||
right = ttk.Frame(body)
|
||||
right.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
self._docs_view = self._readonly_text(right, catalog[0][2])
|
||||
self._docs_view.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
self._docs_list.selection_set(0)
|
||||
self._docs_list.see(0)
|
||||
|
||||
def _on_docs_select(self, _event=None) -> None:
|
||||
if not hasattr(self, "_docs_list") or not hasattr(self, "_docs_catalog"):
|
||||
return
|
||||
sel = self._docs_list.curselection()
|
||||
if not sel:
|
||||
return
|
||||
idx = int(sel[0])
|
||||
if idx < 0 or idx >= len(self._docs_catalog):
|
||||
return
|
||||
text = self._docs_catalog[idx][2]
|
||||
self._docs_view.configure(state=tk.NORMAL)
|
||||
self._docs_view.delete("1.0", tk.END)
|
||||
self._docs_view.insert("1.0", text)
|
||||
self._docs_view.configure(state=tk.DISABLED)
|
||||
|
||||
def _build_schemes_help(self, parent) -> None:
|
||||
ttk.Label(
|
||||
parent,
|
||||
@@ -553,7 +625,9 @@ class Md2GostApp:
|
||||
catalog = load_prompt_catalog()
|
||||
ttk.Label(
|
||||
parent,
|
||||
text="Скопируйте промпт целиком и вставьте в ChatGPT / Cursor / Copilot. Следующим сообщением — тип, тема, черновик.",
|
||||
text="Скопируйте промпт и вставьте в ChatGPT / Cursor / Copilot. "
|
||||
"Кнопки схем дописывают макросы из md2gost.schemes.json — "
|
||||
"когда ИИ должен знать C4/BPMN/свои шаблоны. Следующим сообщением — тип, тема, черновик.",
|
||||
style="Hint.TLabel",
|
||||
wraplength=640,
|
||||
).pack(anchor=tk.W, pady=(0, 8))
|
||||
@@ -575,32 +649,113 @@ class Md2GostApp:
|
||||
ttk.Label(row, text="Промпт").pack(side=tk.LEFT, padx=(0, 8))
|
||||
box = ttk.Combobox(row, textvariable=self._prompt_choice, values=titles, state="readonly")
|
||||
box.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
||||
box.bind("<<ComboboxSelected>>", lambda _e: self._show_selected_prompt())
|
||||
box.bind("<<ComboboxSelected>>", lambda _e: self._refresh_composed_prompt())
|
||||
btn = ttk.Button(row, text="Скопировать", command=self._copy_selected_prompt)
|
||||
btn.pack(side=tk.LEFT, padx=(8, 0))
|
||||
_tip(btn, "Скопировать выбранный промпт в буфер обмена")
|
||||
_tip(btn, "Скопировать собранный промпт (база + выбранные схемы) в буфер")
|
||||
|
||||
self._prompt_view = self._readonly_text(parent, self._prompt_texts[titles[0]])
|
||||
schemes_frame = ttk.LabelFrame(parent, text="Дописать схемы в промпт", padding=6)
|
||||
schemes_frame.pack(fill=tk.X, pady=(0, 6))
|
||||
ttk.Label(
|
||||
schemes_frame,
|
||||
text="UML/Mermaid и так конвертируются в Рисунок; схемы нужны для макросов.",
|
||||
style="Hint.TLabel",
|
||||
wraplength=620,
|
||||
).pack(anchor=tk.W, pady=(0, 4))
|
||||
|
||||
try:
|
||||
ensure_user_schemes()
|
||||
schemes_map = get_schemes()
|
||||
except Exception:
|
||||
schemes_map = {}
|
||||
self._prompt_scheme_ids = sorted(schemes_map.keys())
|
||||
self._prompt_scheme_vars: dict[str, tk.BooleanVar] = {}
|
||||
self._prompt_schemes_by_id = dict(schemes_map)
|
||||
|
||||
toggles = ttk.Frame(schemes_frame)
|
||||
toggles.pack(fill=tk.X)
|
||||
if not self._prompt_scheme_ids:
|
||||
ttk.Label(
|
||||
toggles,
|
||||
text="Схемы не найдены (md2gost.schemes.json).",
|
||||
style="Hint.TLabel",
|
||||
).pack(anchor=tk.W)
|
||||
else:
|
||||
for sid in self._prompt_scheme_ids:
|
||||
var = tk.BooleanVar(value=False)
|
||||
self._prompt_scheme_vars[sid] = var
|
||||
title = schemes_map[sid].title or sid
|
||||
cb = ttk.Checkbutton(
|
||||
toggles,
|
||||
text=f"{title} ({sid})",
|
||||
variable=var,
|
||||
command=self._refresh_composed_prompt,
|
||||
)
|
||||
cb.pack(anchor=tk.W)
|
||||
_tip(cb, f"Добавить в промпт макросы схемы ```uml-{sid}")
|
||||
|
||||
ctrl = ttk.Frame(schemes_frame)
|
||||
ctrl.pack(fill=tk.X, pady=(4, 0))
|
||||
btn_all = ttk.Button(ctrl, text="Все", command=self._prompt_schemes_select_all)
|
||||
btn_all.pack(side=tk.LEFT, padx=(0, 6))
|
||||
_tip(btn_all, "Включить все схемы")
|
||||
btn_none = ttk.Button(ctrl, text="Сбросить", command=self._prompt_schemes_clear)
|
||||
btn_none.pack(side=tk.LEFT)
|
||||
_tip(btn_none, "Выключить все схемы")
|
||||
|
||||
self._prompt_view = self._readonly_text(parent, "")
|
||||
self._prompt_view.pack(fill=tk.BOTH, expand=True)
|
||||
self._copy_status = ttk.Label(parent, text="", style="Hint.TLabel")
|
||||
self._copy_status.pack(anchor=tk.W, pady=(6, 0))
|
||||
self._refresh_composed_prompt()
|
||||
|
||||
def _show_selected_prompt(self) -> None:
|
||||
text = self._prompt_texts.get(self._prompt_choice.get(), "")
|
||||
def _selected_prompt_schemes(self) -> list:
|
||||
out = []
|
||||
for sid in getattr(self, "_prompt_scheme_ids", []):
|
||||
var = self._prompt_scheme_vars.get(sid)
|
||||
if var is not None and var.get():
|
||||
scheme = self._prompt_schemes_by_id.get(sid)
|
||||
if scheme is not None:
|
||||
out.append(scheme)
|
||||
return out
|
||||
|
||||
def _composed_prompt_text(self) -> str:
|
||||
base = self._prompt_texts.get(self._prompt_choice.get(), "")
|
||||
return compose_prompt(base, self._selected_prompt_schemes())
|
||||
|
||||
def _refresh_composed_prompt(self) -> None:
|
||||
if not hasattr(self, "_prompt_view"):
|
||||
return
|
||||
text = self._composed_prompt_text()
|
||||
self._prompt_view.configure(state=tk.NORMAL)
|
||||
self._prompt_view.delete("1.0", tk.END)
|
||||
self._prompt_view.insert("1.0", text)
|
||||
self._prompt_view.configure(state=tk.DISABLED)
|
||||
self._copy_status.configure(text="")
|
||||
if hasattr(self, "_copy_status"):
|
||||
self._copy_status.configure(text="")
|
||||
|
||||
def _prompt_schemes_select_all(self) -> None:
|
||||
for var in self._prompt_scheme_vars.values():
|
||||
var.set(True)
|
||||
self._refresh_composed_prompt()
|
||||
|
||||
def _prompt_schemes_clear(self) -> None:
|
||||
for var in self._prompt_scheme_vars.values():
|
||||
var.set(False)
|
||||
self._refresh_composed_prompt()
|
||||
|
||||
def _copy_selected_prompt(self) -> None:
|
||||
text = self._prompt_texts.get(self._prompt_choice.get(), "")
|
||||
text = self._composed_prompt_text()
|
||||
if not text:
|
||||
return
|
||||
self.root.clipboard_clear()
|
||||
self.root.clipboard_append(text)
|
||||
self.root.update_idletasks()
|
||||
self._copy_status.configure(text="Скопировано в буфер обмена — можно вставлять в ИИ.")
|
||||
n = len(self._selected_prompt_schemes())
|
||||
extra = f" (+{n} схем)" if n else ""
|
||||
self._copy_status.configure(
|
||||
text=f"Скопировано в буфер обмена{extra} — можно вставлять в ИИ."
|
||||
)
|
||||
|
||||
def _readonly_text(self, parent, content: str) -> tk.Text:
|
||||
wrap = ttk.Frame(parent)
|
||||
@@ -1065,6 +1220,9 @@ class Md2GostApp:
|
||||
def _browse_assignment(self) -> None:
|
||||
self._pick_into(self.assignment_var, "Бланк задания", [("Word", "*.docx")])
|
||||
|
||||
def _browse_styles(self) -> None:
|
||||
self._pick_into(self.styles_var, "Стили JSON", [("JSON", "*.json"), ("Все файлы", "*.*")])
|
||||
|
||||
def _browse_jar(self) -> None:
|
||||
self._pick_into(self.plantuml_var, "Свой plantuml.jar", [("JAR", "*.jar"), ("Все файлы", "*.*")])
|
||||
self._refresh_diagram_status()
|
||||
@@ -1149,6 +1307,7 @@ class Md2GostApp:
|
||||
self.template_var.set(req.template or "")
|
||||
self.title_var.set(req.title or "")
|
||||
self.assignment_var.set(req.assignment or "")
|
||||
self.styles_var.set(getattr(req, "styles_path", None) or "")
|
||||
self.plantuml_var.set(req.plantuml_jar or "")
|
||||
self.kroki_var.set(req.kroki_url or "")
|
||||
self._refresh_diagram_status()
|
||||
@@ -1181,6 +1340,7 @@ class Md2GostApp:
|
||||
diagram_fallback=_combo_key(self.fallback_var.get(), ("remote", "local", "off"), "remote"),
|
||||
diagram_format=_combo_key(self.format_var.get(), ("png", "svg"), "png"),
|
||||
diagram_scale=self._parse_diagram_scale(),
|
||||
styles_path=self.styles_var.get().strip() or None,
|
||||
debug=self.debug_var.get(),
|
||||
open_when_done=self.open_var.get(),
|
||||
)
|
||||
|
||||
+111
-1
@@ -33,6 +33,13 @@ CLI (тот же движок)
|
||||
python -m md2gost --gui
|
||||
md2gost.exe report.md --type PIS_custom --title title.docx
|
||||
md2gost.exe report.md --schemes path/to/md2gost.schemes.json
|
||||
md2gost.exe report.md --styles path/to/md2gost.styles.json
|
||||
|
||||
Стили JSON (опционально)
|
||||
Оверлей поверх пресета типа документа (--type). Файл md2gost.styles.json рядом с .md
|
||||
или --styles / Настройки → Файлы → «Стили JSON». Меняет поля страницы и параметры
|
||||
стилей абзацев (Normal, Heading 1–3, подписи…). В шаблон DOCX стили руками добавлять не нужно.
|
||||
Подробнее: docs/styles.md.
|
||||
|
||||
СИНТАКСИС MARKDOWN
|
||||
|
||||
@@ -126,7 +133,12 @@ IDEF0 конвертер не рисует — вставляйте готовы
|
||||
off — не резать, Word сам переносит.
|
||||
legacy / caption — режем по оценке высоты в md2gost (может не совпасть с Word).
|
||||
|
||||
Промпт для ИИ — Справка → Промпт для ИИ: скопируйте и вставьте в ChatGPT / Cursor / Copilot, затем дайте тему и черновик.
|
||||
Промпт для ИИ — Справка → Промпт для ИИ: выберите промпт, при необходимости
|
||||
включите схемы (C4, BPMN, …) кнопками — макросы допишутся в конец — скопируйте
|
||||
в ChatGPT / Cursor / Copilot, затем дайте тему и черновик.
|
||||
|
||||
Документация — Справка → Документация: встроенный просмотр docs/*.md
|
||||
(вшито в exe; внешняя папка docs/ не обязательна).
|
||||
"""
|
||||
|
||||
SCHEMES_HELP = """СХЕМЫ ДИАГРАММ (PlantUML)
|
||||
@@ -219,6 +231,7 @@ Mermaid: только Kroki (свой --kroki-url / localhost / kroki.io).
|
||||
"""
|
||||
|
||||
PROMPT_FILES = (
|
||||
("generate-md.md", "Markdown для md2gost"),
|
||||
("generate-mirea-report.md", "МИРЭА / ГОСТ (курсовая, практика, ВКР)"),
|
||||
("generate-pis-custom-report.md", "ПИС — отчёт по практическим работам"),
|
||||
)
|
||||
@@ -259,3 +272,100 @@ def load_prompt_catalog() -> list[tuple[str, str, str]]:
|
||||
if text:
|
||||
catalog.append((name, title, text))
|
||||
return catalog
|
||||
|
||||
|
||||
def scheme_prompt_block(scheme) -> str:
|
||||
"""Format one DiagramScheme for appending to an AI prompt."""
|
||||
sid = getattr(scheme, "id", "") or ""
|
||||
title = (getattr(scheme, "title", None) or sid).strip()
|
||||
lines = [
|
||||
f"## Схема: {title} (`{sid}`)",
|
||||
"",
|
||||
f"Оградка в markdown: ```uml-{sid} или ```{sid}",
|
||||
"",
|
||||
]
|
||||
ai = (getattr(scheme, "ai_prompt", None) or "").strip()
|
||||
if ai:
|
||||
lines.append(ai)
|
||||
lines.append("")
|
||||
docs = (getattr(scheme, "docs", None) or "").strip()
|
||||
if docs:
|
||||
lines.append("Макросы / шпаргалка:")
|
||||
lines.append(docs)
|
||||
lines.append("")
|
||||
return "\n".join(lines).rstrip() + "\n"
|
||||
|
||||
|
||||
def compose_prompt(base: str, schemes: list | None = None) -> str:
|
||||
"""
|
||||
Base prompt text plus optional scheme blocks (order preserved).
|
||||
schemes: iterable of DiagramScheme (or objects with id/title/docs/ai_prompt).
|
||||
"""
|
||||
text = (base or "").rstrip()
|
||||
if not schemes:
|
||||
return text + ("\n" if text else "")
|
||||
parts = [text, "", "---", "", "# Дополнение: выбранные схемы диаграмм", ""]
|
||||
for scheme in schemes:
|
||||
parts.append(scheme_prompt_block(scheme))
|
||||
parts.append("")
|
||||
return "\n".join(parts).rstrip() + "\n"
|
||||
|
||||
|
||||
def docs_search_dirs() -> list[Path]:
|
||||
dirs: list[Path] = []
|
||||
here = Path(package_dir())
|
||||
dirs.append(here / "docs")
|
||||
dirs.append(here.parent / "docs")
|
||||
if getattr(sys, "frozen", False):
|
||||
mei = getattr(sys, "_MEIPASS", None)
|
||||
if mei:
|
||||
dirs.append(Path(mei) / "docs")
|
||||
dirs.append(Path(sys.executable).resolve().parent / "docs")
|
||||
seen: set[str] = set()
|
||||
out: list[Path] = []
|
||||
for path in dirs:
|
||||
key = str(path.resolve()) if path.exists() else str(path)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
out.append(path)
|
||||
return out
|
||||
|
||||
|
||||
def _doc_title_from_text(filename: str, text: str) -> str:
|
||||
for line in text.splitlines():
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("#"):
|
||||
return stripped.lstrip("#").strip() or Path(filename).stem
|
||||
return Path(filename).stem
|
||||
|
||||
|
||||
def load_docs_catalog() -> list[tuple[str, str, str]]:
|
||||
"""
|
||||
Return list of (filename, title, text) from the first existing docs/ folder.
|
||||
README.md first, then other *.md alphabetically.
|
||||
"""
|
||||
folder: Path | None = None
|
||||
for path in docs_search_dirs():
|
||||
if path.is_dir():
|
||||
folder = path
|
||||
break
|
||||
if folder is None:
|
||||
return []
|
||||
|
||||
files = sorted(p for p in folder.glob("*.md") if p.is_file())
|
||||
if not files:
|
||||
return []
|
||||
|
||||
readme = [p for p in files if p.name.lower() == "readme.md"]
|
||||
rest = [p for p in files if p.name.lower() != "readme.md"]
|
||||
ordered = readme + rest
|
||||
|
||||
catalog: list[tuple[str, str, str]] = []
|
||||
for path in ordered:
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
except OSError:
|
||||
continue
|
||||
catalog.append((path.name, _doc_title_from_text(path.name, text), text))
|
||||
return catalog
|
||||
|
||||
+6
-4
@@ -22,7 +22,6 @@ from .profiles import (
|
||||
DEFAULT_TABLE_CONTINUATION,
|
||||
DEFAULT_LISTING_CONTINUATION,
|
||||
DEFAULT_TOC_MODE,
|
||||
get_profile,
|
||||
)
|
||||
|
||||
LogFn = Callable[[str], None]
|
||||
@@ -52,6 +51,7 @@ class ConvertRequest:
|
||||
diagram_format: str = "png"
|
||||
diagram_scale: float = 2.0
|
||||
schemes_path: str | None = None
|
||||
styles_path: str | None = None
|
||||
debug: bool = False
|
||||
open_when_done: bool = False
|
||||
check_pages: bool = False
|
||||
@@ -219,6 +219,7 @@ def convert(req: ConvertRequest, log: LogFn | None = None) -> ConvertResult:
|
||||
table_continuation=req.table_continuation,
|
||||
listing_continuation=req.listing_continuation,
|
||||
hr_pagebreak=req.hr_pagebreak,
|
||||
styles_path=req.styles_path or None,
|
||||
)
|
||||
converter.convert()
|
||||
document = converter.document
|
||||
@@ -228,10 +229,11 @@ def convert(req: ConvertRequest, log: LogFn | None = None) -> ConvertResult:
|
||||
from docxcompose.composer import Composer
|
||||
except ImportError:
|
||||
return fail(3, "Для титула/задания нужен пакет docxcompose", check_report)
|
||||
from .styles import apply_document_styles
|
||||
from .styles import apply_style_config
|
||||
|
||||
style_cfg = converter.style_config
|
||||
shell = Document(template)
|
||||
apply_document_styles(shell, get_profile(req.doc_type).style_preset)
|
||||
apply_style_config(shell, style_cfg)
|
||||
body = shell.element.body
|
||||
for child in list(body):
|
||||
if not child.tag.endswith("}sectPr"):
|
||||
@@ -246,7 +248,7 @@ def convert(req: ConvertRequest, log: LogFn | None = None) -> ConvertResult:
|
||||
shell.add_page_break()
|
||||
composer.append(document)
|
||||
document = composer.doc
|
||||
apply_document_styles(document, get_profile(req.doc_type).style_preset)
|
||||
apply_style_config(document, style_cfg)
|
||||
_fix_front_matter_after_compose(
|
||||
document,
|
||||
had_title=bool(req.title),
|
||||
|
||||
@@ -0,0 +1,597 @@
|
||||
"""JSON overlay for document styles (page margins + paragraph styles).
|
||||
|
||||
Layers (later wins, deep-merge of specified keys only):
|
||||
1) built-in preset from DocProfile.style_preset (mirea | pis_custom)
|
||||
2) md2gost.styles.json next to the .md (if present)
|
||||
3) --styles path / GUI path
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import logging
|
||||
from dataclasses import dataclass, field, fields
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
STYLES_FILENAME = "md2gost.styles.json"
|
||||
|
||||
PAGE_KEYS = frozenset({"left_mm", "right_mm", "top_mm", "bottom_mm"})
|
||||
|
||||
STYLE_NAMES = frozenset({
|
||||
"Normal",
|
||||
"Heading 1",
|
||||
"Heading 2",
|
||||
"Heading 3",
|
||||
"Caption Figure",
|
||||
"Caption Table",
|
||||
"Название таблицы",
|
||||
"Caption Listing",
|
||||
"Caption",
|
||||
"Code",
|
||||
"Table Text",
|
||||
"Bibliography",
|
||||
"Bibliography Heading",
|
||||
"toc 1",
|
||||
"toc 2",
|
||||
"toc 3",
|
||||
"Footer",
|
||||
"Hyperlink",
|
||||
"FollowedHyperlink",
|
||||
"Space After Table",
|
||||
})
|
||||
|
||||
ALIGNMENT_VALUES = frozenset({"left", "center", "justify"})
|
||||
LINE_SPACING_VALUES = frozenset({1.0, 1.5})
|
||||
|
||||
STYLE_FIELD_KEYS = frozenset({
|
||||
"font_name",
|
||||
"size_pt",
|
||||
"bold",
|
||||
"italic",
|
||||
"all_caps",
|
||||
"underline",
|
||||
"alignment",
|
||||
"first_line_indent_cm",
|
||||
"left_indent_cm",
|
||||
"right_indent_cm",
|
||||
"space_before_mm",
|
||||
"space_after_mm",
|
||||
"line_spacing",
|
||||
"page_break_before",
|
||||
"keep_with_next",
|
||||
"widow_control",
|
||||
})
|
||||
|
||||
TOP_LEVEL_KEYS = frozenset({"page", "styles"})
|
||||
|
||||
|
||||
class StyleConfigError(ValueError):
|
||||
"""Invalid styles JSON or unknown key/style name."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class PageSpec:
|
||||
left_mm: float | None = None
|
||||
right_mm: float | None = None
|
||||
top_mm: float | None = None
|
||||
bottom_mm: float | None = None
|
||||
|
||||
def merge(self, overlay: "PageSpec | dict[str, Any] | None") -> "PageSpec":
|
||||
if overlay is None:
|
||||
return copy.copy(self)
|
||||
if isinstance(overlay, PageSpec):
|
||||
data = {
|
||||
f.name: getattr(overlay, f.name)
|
||||
for f in fields(overlay)
|
||||
if getattr(overlay, f.name) is not None
|
||||
}
|
||||
else:
|
||||
data = dict(overlay)
|
||||
out = copy.copy(self)
|
||||
for key, val in data.items():
|
||||
if key not in PAGE_KEYS:
|
||||
raise StyleConfigError(f"Unknown page key: {key!r}")
|
||||
setattr(out, key, float(val))
|
||||
return out
|
||||
|
||||
def to_dict(self) -> dict[str, float]:
|
||||
out: dict[str, float] = {}
|
||||
for f in fields(self):
|
||||
val = getattr(self, f.name)
|
||||
if val is not None:
|
||||
out[f.name] = val
|
||||
return out
|
||||
|
||||
def require_complete(self) -> "PageSpec":
|
||||
"""Ensure all margins are set (for apply). Missing → GOST defaults."""
|
||||
return PageSpec(
|
||||
left_mm=30.0 if self.left_mm is None else self.left_mm,
|
||||
right_mm=10.0 if self.right_mm is None else self.right_mm,
|
||||
top_mm=20.0 if self.top_mm is None else self.top_mm,
|
||||
bottom_mm=20.0 if self.bottom_mm is None else self.bottom_mm,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ParagraphStyleSpec:
|
||||
font_name: str | None = None
|
||||
size_pt: float | None = None
|
||||
bold: bool | None = None
|
||||
italic: bool | None = None
|
||||
all_caps: bool | None = None
|
||||
underline: bool | None = None
|
||||
alignment: str | None = None # left | center | justify
|
||||
first_line_indent_cm: float | None = None
|
||||
left_indent_cm: float | None = None
|
||||
right_indent_cm: float | None = None
|
||||
space_before_mm: float | None = None
|
||||
space_after_mm: float | None = None
|
||||
line_spacing: float | None = None # 1.0 | 1.5
|
||||
page_break_before: bool | None = None
|
||||
keep_with_next: bool | None = None
|
||||
widow_control: bool | None = None
|
||||
|
||||
def merge(self, overlay: "ParagraphStyleSpec | dict[str, Any] | None") -> "ParagraphStyleSpec":
|
||||
if overlay is None:
|
||||
return copy.copy(self)
|
||||
if isinstance(overlay, ParagraphStyleSpec):
|
||||
data = {
|
||||
f.name: getattr(overlay, f.name)
|
||||
for f in fields(overlay)
|
||||
if getattr(overlay, f.name) is not None
|
||||
}
|
||||
else:
|
||||
data = dict(overlay)
|
||||
out = copy.copy(self)
|
||||
for key, val in data.items():
|
||||
if key not in STYLE_FIELD_KEYS:
|
||||
raise StyleConfigError(f"Unknown style field: {key!r}")
|
||||
if val is None:
|
||||
continue
|
||||
if key == "alignment":
|
||||
s = str(val).lower()
|
||||
if s not in ALIGNMENT_VALUES:
|
||||
raise StyleConfigError(
|
||||
f"Invalid alignment {val!r}; expected one of {sorted(ALIGNMENT_VALUES)}"
|
||||
)
|
||||
setattr(out, key, s)
|
||||
elif key == "line_spacing":
|
||||
fval = float(val)
|
||||
if fval not in LINE_SPACING_VALUES:
|
||||
raise StyleConfigError(
|
||||
f"Invalid line_spacing {val!r}; expected 1.0 or 1.5"
|
||||
)
|
||||
setattr(out, key, fval)
|
||||
elif key in ("font_name",):
|
||||
setattr(out, key, str(val))
|
||||
elif key in ("size_pt", "first_line_indent_cm", "left_indent_cm",
|
||||
"right_indent_cm", "space_before_mm", "space_after_mm"):
|
||||
setattr(out, key, float(val))
|
||||
elif key in ("bold", "italic", "all_caps", "underline",
|
||||
"page_break_before", "keep_with_next", "widow_control"):
|
||||
setattr(out, key, bool(val))
|
||||
else:
|
||||
setattr(out, key, val)
|
||||
return out
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
out: dict[str, Any] = {}
|
||||
for f in fields(self):
|
||||
val = getattr(self, f.name)
|
||||
if val is not None:
|
||||
out[f.name] = val
|
||||
return out
|
||||
|
||||
|
||||
@dataclass
|
||||
class StyleConfig:
|
||||
page: PageSpec = field(default_factory=PageSpec)
|
||||
styles: dict[str, ParagraphStyleSpec] = field(default_factory=dict)
|
||||
|
||||
def merge(self, overlay: "StyleConfig | None") -> "StyleConfig":
|
||||
if overlay is None:
|
||||
return copy.deepcopy(self)
|
||||
page = self.page.merge(overlay.page)
|
||||
styles = {name: copy.copy(spec) for name, spec in self.styles.items()}
|
||||
for name, spec in overlay.styles.items():
|
||||
if name not in STYLE_NAMES:
|
||||
raise StyleConfigError(f"Unknown style name: {name!r}")
|
||||
if name in styles:
|
||||
styles[name] = styles[name].merge(spec)
|
||||
else:
|
||||
styles[name] = copy.copy(spec)
|
||||
return StyleConfig(page=page, styles=styles)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"page": self.page.to_dict(),
|
||||
"styles": {name: spec.to_dict() for name, spec in self.styles.items()},
|
||||
}
|
||||
|
||||
|
||||
def _ps(**kwargs) -> ParagraphStyleSpec:
|
||||
return ParagraphStyleSpec(**kwargs)
|
||||
|
||||
|
||||
def _common_body_and_misc() -> dict[str, ParagraphStyleSpec]:
|
||||
"""Shared Normal + captions/misc (identical for mirea and pis_custom)."""
|
||||
return {
|
||||
"Normal": _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=14,
|
||||
bold=False,
|
||||
italic=False,
|
||||
alignment="justify",
|
||||
first_line_indent_cm=1.25,
|
||||
left_indent_cm=0,
|
||||
right_indent_cm=0,
|
||||
space_before_mm=0,
|
||||
space_after_mm=0,
|
||||
line_spacing=1.5,
|
||||
widow_control=True,
|
||||
),
|
||||
"Caption Figure": _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=12,
|
||||
bold=True,
|
||||
italic=False,
|
||||
alignment="center",
|
||||
first_line_indent_cm=0,
|
||||
left_indent_cm=0,
|
||||
space_before_mm=0,
|
||||
space_after_mm=6,
|
||||
line_spacing=1.0,
|
||||
widow_control=True,
|
||||
),
|
||||
"Caption Table": _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=12,
|
||||
bold=False,
|
||||
italic=True,
|
||||
alignment="left",
|
||||
first_line_indent_cm=0,
|
||||
left_indent_cm=0,
|
||||
space_before_mm=6,
|
||||
space_after_mm=0,
|
||||
line_spacing=1.0,
|
||||
keep_with_next=True,
|
||||
widow_control=True,
|
||||
),
|
||||
"Название таблицы": _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=12,
|
||||
bold=False,
|
||||
italic=True,
|
||||
alignment="left",
|
||||
first_line_indent_cm=0,
|
||||
left_indent_cm=0,
|
||||
space_before_mm=6,
|
||||
space_after_mm=0,
|
||||
line_spacing=1.0,
|
||||
keep_with_next=True,
|
||||
widow_control=True,
|
||||
),
|
||||
"Caption Listing": _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=12,
|
||||
bold=False,
|
||||
italic=True,
|
||||
alignment="left",
|
||||
first_line_indent_cm=0,
|
||||
left_indent_cm=0,
|
||||
space_before_mm=6,
|
||||
space_after_mm=0,
|
||||
line_spacing=1.0,
|
||||
keep_with_next=True,
|
||||
widow_control=True,
|
||||
),
|
||||
"Caption": _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=12,
|
||||
bold=True,
|
||||
italic=False,
|
||||
alignment="center",
|
||||
first_line_indent_cm=0,
|
||||
space_before_mm=0,
|
||||
space_after_mm=6,
|
||||
line_spacing=1.0,
|
||||
),
|
||||
"Code": _ps(
|
||||
font_name="Courier New",
|
||||
size_pt=10,
|
||||
bold=False,
|
||||
italic=False,
|
||||
alignment="left",
|
||||
first_line_indent_cm=0,
|
||||
left_indent_cm=0,
|
||||
space_before_mm=0,
|
||||
space_after_mm=0,
|
||||
line_spacing=1.0,
|
||||
),
|
||||
"Table Text": _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=12,
|
||||
bold=False,
|
||||
italic=False,
|
||||
alignment="left",
|
||||
first_line_indent_cm=0,
|
||||
left_indent_cm=0,
|
||||
space_before_mm=0,
|
||||
space_after_mm=0,
|
||||
line_spacing=1.0,
|
||||
),
|
||||
"Bibliography": _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=14,
|
||||
bold=False,
|
||||
italic=False,
|
||||
alignment="justify",
|
||||
first_line_indent_cm=1.25,
|
||||
space_before_mm=0,
|
||||
space_after_mm=0,
|
||||
line_spacing=1.5,
|
||||
),
|
||||
"Bibliography Heading": _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=14,
|
||||
bold=False,
|
||||
italic=False,
|
||||
all_caps=True,
|
||||
alignment="center",
|
||||
first_line_indent_cm=0,
|
||||
left_indent_cm=1.25,
|
||||
space_before_mm=6,
|
||||
space_after_mm=6,
|
||||
line_spacing=1.5,
|
||||
keep_with_next=True,
|
||||
),
|
||||
"toc 1": _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=14,
|
||||
bold=False,
|
||||
all_caps=True,
|
||||
alignment="left",
|
||||
first_line_indent_cm=0,
|
||||
space_before_mm=0,
|
||||
space_after_mm=0,
|
||||
line_spacing=1.5,
|
||||
),
|
||||
"toc 2": _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=14,
|
||||
bold=False,
|
||||
all_caps=False,
|
||||
alignment="left",
|
||||
first_line_indent_cm=0,
|
||||
space_before_mm=0,
|
||||
space_after_mm=0,
|
||||
line_spacing=1.5,
|
||||
),
|
||||
"toc 3": _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=14,
|
||||
bold=False,
|
||||
all_caps=False,
|
||||
alignment="left",
|
||||
first_line_indent_cm=0,
|
||||
space_before_mm=0,
|
||||
space_after_mm=0,
|
||||
line_spacing=1.5,
|
||||
),
|
||||
"Footer": _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=12,
|
||||
alignment="center",
|
||||
first_line_indent_cm=0,
|
||||
),
|
||||
"Hyperlink": _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=14,
|
||||
underline=False,
|
||||
),
|
||||
"FollowedHyperlink": _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=14,
|
||||
underline=False,
|
||||
),
|
||||
"Space After Table": _ps(
|
||||
first_line_indent_cm=1.25,
|
||||
space_before_mm=6,
|
||||
space_after_mm=0,
|
||||
line_spacing=1.5,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def preset_mirea() -> StyleConfig:
|
||||
styles = _common_body_and_misc()
|
||||
styles["Heading 1"] = _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=18,
|
||||
bold=True,
|
||||
all_caps=True,
|
||||
alignment="left",
|
||||
first_line_indent_cm=0,
|
||||
left_indent_cm=1.25,
|
||||
right_indent_cm=0,
|
||||
space_before_mm=0,
|
||||
space_after_mm=10,
|
||||
line_spacing=1.5,
|
||||
page_break_before=True,
|
||||
keep_with_next=True,
|
||||
widow_control=True,
|
||||
)
|
||||
styles["Heading 2"] = _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=16,
|
||||
bold=True,
|
||||
all_caps=False,
|
||||
alignment="left",
|
||||
first_line_indent_cm=0,
|
||||
left_indent_cm=1.25,
|
||||
right_indent_cm=0,
|
||||
space_before_mm=15,
|
||||
space_after_mm=10,
|
||||
line_spacing=1.5,
|
||||
page_break_before=False,
|
||||
keep_with_next=True,
|
||||
widow_control=True,
|
||||
)
|
||||
styles["Heading 3"] = _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=14,
|
||||
bold=True,
|
||||
all_caps=False,
|
||||
alignment="left",
|
||||
first_line_indent_cm=0,
|
||||
left_indent_cm=1.25,
|
||||
right_indent_cm=0,
|
||||
space_before_mm=15,
|
||||
space_after_mm=10,
|
||||
line_spacing=1.5,
|
||||
page_break_before=False,
|
||||
keep_with_next=True,
|
||||
widow_control=True,
|
||||
)
|
||||
return StyleConfig(
|
||||
page=PageSpec(left_mm=30, right_mm=10, top_mm=20, bottom_mm=20),
|
||||
styles=styles,
|
||||
)
|
||||
|
||||
|
||||
def preset_pis_custom() -> StyleConfig:
|
||||
styles = _common_body_and_misc()
|
||||
styles["Heading 1"] = _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=14,
|
||||
bold=True,
|
||||
all_caps=True,
|
||||
alignment="center",
|
||||
first_line_indent_cm=0,
|
||||
left_indent_cm=0,
|
||||
right_indent_cm=0,
|
||||
space_before_mm=0,
|
||||
space_after_mm=10,
|
||||
line_spacing=1.5,
|
||||
page_break_before=True,
|
||||
keep_with_next=True,
|
||||
widow_control=True,
|
||||
)
|
||||
styles["Heading 2"] = _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=14,
|
||||
bold=True,
|
||||
all_caps=False,
|
||||
alignment="justify",
|
||||
first_line_indent_cm=1.25,
|
||||
left_indent_cm=0,
|
||||
right_indent_cm=0,
|
||||
space_before_mm=15,
|
||||
space_after_mm=10,
|
||||
line_spacing=1.5,
|
||||
page_break_before=False,
|
||||
keep_with_next=True,
|
||||
widow_control=True,
|
||||
)
|
||||
styles["Heading 3"] = _ps(
|
||||
font_name="Times New Roman",
|
||||
size_pt=14,
|
||||
bold=True,
|
||||
all_caps=False,
|
||||
alignment="justify",
|
||||
first_line_indent_cm=1.25,
|
||||
left_indent_cm=0,
|
||||
right_indent_cm=0,
|
||||
space_before_mm=10,
|
||||
space_after_mm=10,
|
||||
line_spacing=1.5,
|
||||
page_break_before=False,
|
||||
keep_with_next=True,
|
||||
widow_control=True,
|
||||
)
|
||||
return StyleConfig(
|
||||
page=PageSpec(left_mm=30, right_mm=10, top_mm=20, bottom_mm=20),
|
||||
styles=styles,
|
||||
)
|
||||
|
||||
|
||||
PRESETS: dict[str, Callable[[], StyleConfig]] = {
|
||||
"mirea": preset_mirea,
|
||||
"pis_custom": preset_pis_custom,
|
||||
}
|
||||
|
||||
|
||||
def get_preset(name: str) -> StyleConfig:
|
||||
factory = PRESETS.get(name, preset_mirea)
|
||||
return factory()
|
||||
|
||||
|
||||
def style_config_from_dict(data: dict[str, Any]) -> StyleConfig:
|
||||
if not isinstance(data, dict):
|
||||
raise StyleConfigError("Styles JSON root must be an object")
|
||||
unknown = set(data.keys()) - TOP_LEVEL_KEYS
|
||||
if unknown:
|
||||
raise StyleConfigError(f"Unknown top-level key(s): {sorted(unknown)}")
|
||||
|
||||
page = PageSpec()
|
||||
if "page" in data:
|
||||
raw_page = data["page"]
|
||||
if not isinstance(raw_page, dict):
|
||||
raise StyleConfigError("'page' must be an object")
|
||||
page = PageSpec().merge(raw_page)
|
||||
|
||||
styles: dict[str, ParagraphStyleSpec] = {}
|
||||
if "styles" in data:
|
||||
raw_styles = data["styles"]
|
||||
if not isinstance(raw_styles, dict):
|
||||
raise StyleConfigError("'styles' must be an object")
|
||||
for name, raw_spec in raw_styles.items():
|
||||
if name not in STYLE_NAMES:
|
||||
raise StyleConfigError(f"Unknown style name: {name!r}")
|
||||
if not isinstance(raw_spec, dict):
|
||||
raise StyleConfigError(f"Style {name!r} must be an object")
|
||||
styles[name] = ParagraphStyleSpec().merge(raw_spec)
|
||||
|
||||
return StyleConfig(page=page, styles=styles)
|
||||
|
||||
|
||||
def load_styles_file(path: Path) -> StyleConfig:
|
||||
if not path.is_file():
|
||||
raise StyleConfigError(f"Styles file not found: {path}")
|
||||
try:
|
||||
raw = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
raise StyleConfigError(f"Cannot read styles {path}: {exc}") from exc
|
||||
if not isinstance(raw, dict):
|
||||
raise StyleConfigError(f"Styles JSON root must be an object: {path}")
|
||||
return style_config_from_dict(raw)
|
||||
|
||||
|
||||
def resolve_style_config(
|
||||
style_preset: str,
|
||||
*,
|
||||
md_dir: str | Path | None = None,
|
||||
styles_path: str | Path | None = None,
|
||||
) -> StyleConfig:
|
||||
"""Merge preset ← near-md file ← explicit --styles path."""
|
||||
config = get_preset(style_preset)
|
||||
|
||||
if md_dir:
|
||||
near = Path(md_dir) / STYLES_FILENAME
|
||||
if near.is_file():
|
||||
try:
|
||||
config = config.merge(load_styles_file(near))
|
||||
_log.info("Applied styles overlay: %s", near)
|
||||
except StyleConfigError:
|
||||
raise
|
||||
|
||||
if styles_path:
|
||||
path = Path(styles_path)
|
||||
config = config.merge(load_styles_file(path))
|
||||
_log.info("Applied styles overlay: %s", path)
|
||||
|
||||
return config
|
||||
+156
-230
@@ -1,5 +1,7 @@
|
||||
"""Apply MIREA TZ (GOST 7.32 / методичка 2022) paragraph styles to a Document."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from docx.document import Document
|
||||
from docx.enum.style import WD_STYLE_TYPE
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING, WD_TAB_ALIGNMENT, WD_TAB_LEADER
|
||||
@@ -7,6 +9,32 @@ from docx.oxml.ns import qn
|
||||
from docx.shared import Cm, Mm, Pt
|
||||
from docx.styles.style import _ParagraphStyle as ParagraphStyle
|
||||
|
||||
from .style_config import (
|
||||
ParagraphStyleSpec,
|
||||
StyleConfig,
|
||||
get_preset,
|
||||
resolve_style_config,
|
||||
)
|
||||
|
||||
_ALIGN = {
|
||||
"left": WD_ALIGN_PARAGRAPH.LEFT,
|
||||
"center": WD_ALIGN_PARAGRAPH.CENTER,
|
||||
"justify": WD_ALIGN_PARAGRAPH.JUSTIFY,
|
||||
}
|
||||
|
||||
# Styles that may be missing from Template.docx and need ensure + base
|
||||
_ENSURE_BASE = {
|
||||
"Caption Figure": "Caption",
|
||||
"Caption Table": "Caption",
|
||||
"Название таблицы": "Caption Table",
|
||||
"Caption Listing": "Caption",
|
||||
"Code": "Normal",
|
||||
"Table Text": "Normal",
|
||||
"Bibliography": "Normal",
|
||||
"Bibliography Heading": "Normal",
|
||||
"Space After Table": "Normal",
|
||||
}
|
||||
|
||||
|
||||
def _set_run_font(style: ParagraphStyle, name: str, size_pt: float, bold: bool = False, italic: bool = False):
|
||||
"""Lock typeface/size/color so Word theme (Calibri + accent blue) cannot leak."""
|
||||
@@ -55,6 +83,18 @@ def _ensure_style(document: Document, name: str, base: str = "Normal") -> Paragr
|
||||
return style
|
||||
|
||||
|
||||
def _get_or_ensure(document: Document, name: str) -> ParagraphStyle | None:
|
||||
base = _ENSURE_BASE.get(name)
|
||||
if base is not None:
|
||||
return _ensure_style(document, name, base)
|
||||
try:
|
||||
return document.styles[name]
|
||||
except KeyError:
|
||||
if name.startswith("toc "):
|
||||
return _ensure_style(document, name, "Normal")
|
||||
return None
|
||||
|
||||
|
||||
def _clear_tab_stops(style: ParagraphStyle) -> None:
|
||||
pPr = style.element.get_or_add_pPr()
|
||||
tabs = pPr.find(qn("w:tabs"))
|
||||
@@ -88,249 +128,135 @@ def _fix_toc_tab_stops(document: Document) -> None:
|
||||
)
|
||||
|
||||
|
||||
def apply_mirea_styles(document: Document) -> None:
|
||||
"""Mutate section margins and key paragraph styles to match the MIREA method guide."""
|
||||
_apply_common_page_and_body(document)
|
||||
def _apply_paragraph_spec(style, spec: ParagraphStyleSpec) -> None:
|
||||
need_font = (
|
||||
spec.font_name is not None
|
||||
or spec.size_pt is not None
|
||||
or spec.bold is not None
|
||||
or spec.italic is not None
|
||||
)
|
||||
if need_font:
|
||||
name = spec.font_name if spec.font_name is not None else (style.font.name or "Times New Roman")
|
||||
size = spec.size_pt if spec.size_pt is not None else (
|
||||
style.font.size.pt if style.font.size else 14
|
||||
)
|
||||
bold = bool(spec.bold) if spec.bold is not None else bool(style.font.bold)
|
||||
italic = bool(spec.italic) if spec.italic is not None else bool(style.font.italic)
|
||||
_set_run_font(style, name, size, bold=bold, italic=italic)
|
||||
|
||||
# --- Headings (табл. 2.1): слева с отступом 1,25 см ---
|
||||
heading_specs = [
|
||||
(1, 18, Mm(0), Mm(10), True),
|
||||
(2, 16, Mm(15), Mm(10), False),
|
||||
(3, 14, Mm(15), Mm(10), False),
|
||||
]
|
||||
for level, size, before, after, page_break in heading_specs:
|
||||
style: ParagraphStyle = document.styles[f"Heading {level}"]
|
||||
_set_run_font(style, "Times New Roman", size, bold=True)
|
||||
if level == 1:
|
||||
style.font.all_caps = True
|
||||
if spec.all_caps is not None:
|
||||
style.font.all_caps = spec.all_caps
|
||||
if spec.underline is not None:
|
||||
style.font.underline = spec.underline
|
||||
|
||||
# Character styles (Hyperlink, …) have no paragraph_format
|
||||
if style.type != WD_STYLE_TYPE.PARAGRAPH:
|
||||
return
|
||||
|
||||
pf = style.paragraph_format
|
||||
if spec.alignment is not None:
|
||||
pf.alignment = _ALIGN[spec.alignment]
|
||||
if spec.first_line_indent_cm is not None:
|
||||
pf.first_line_indent = Cm(spec.first_line_indent_cm)
|
||||
if spec.left_indent_cm is not None:
|
||||
pf.left_indent = Cm(spec.left_indent_cm)
|
||||
if spec.right_indent_cm is not None:
|
||||
pf.right_indent = Cm(spec.right_indent_cm)
|
||||
if spec.space_before_mm is not None:
|
||||
pf.space_before = Mm(spec.space_before_mm)
|
||||
if spec.space_after_mm is not None:
|
||||
pf.space_after = Mm(spec.space_after_mm)
|
||||
if spec.line_spacing is not None:
|
||||
if spec.line_spacing == 1.5:
|
||||
pf.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
||||
else:
|
||||
style.font.all_caps = False
|
||||
hpf = style.paragraph_format
|
||||
hpf.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
hpf.first_line_indent = Cm(0)
|
||||
hpf.left_indent = Cm(1.25)
|
||||
hpf.right_indent = Cm(0)
|
||||
hpf.space_before = before
|
||||
hpf.space_after = after
|
||||
hpf.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
||||
hpf.page_break_before = page_break
|
||||
hpf.keep_with_next = True
|
||||
hpf.widow_control = True
|
||||
|
||||
_apply_common_captions_and_misc(document)
|
||||
pf.line_spacing_rule = WD_LINE_SPACING.SINGLE
|
||||
if spec.page_break_before is not None:
|
||||
pf.page_break_before = spec.page_break_before
|
||||
if spec.keep_with_next is not None:
|
||||
pf.keep_with_next = spec.keep_with_next
|
||||
if spec.widow_control is not None:
|
||||
pf.widow_control = spec.widow_control
|
||||
|
||||
|
||||
def apply_pis_custom_styles(document: Document) -> None:
|
||||
"""Styles for PIS_custom: итоговый отчёт по практическим работам.
|
||||
def _apply_page_margins(config: StyleConfig) -> None:
|
||||
from . import page_geometry as pg
|
||||
|
||||
* H1 (разделы / практические работы): по центру, ПРОПИСНЫЕ, без точки.
|
||||
* H2 (подразделы): с абзацного отступа 1,25 см, с прописной буквы.
|
||||
* Поля / шрифт / интервал / красная строка — как в чек-листе ПИС (= ГОСТ поля).
|
||||
"""
|
||||
_apply_common_page_and_body(document)
|
||||
|
||||
# H1 — раздел: центр, caps, с новой страницы
|
||||
h1: ParagraphStyle = document.styles["Heading 1"]
|
||||
_set_run_font(h1, "Times New Roman", 14, bold=True)
|
||||
h1.font.all_caps = True
|
||||
h1pf = h1.paragraph_format
|
||||
h1pf.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
h1pf.first_line_indent = Cm(0)
|
||||
h1pf.left_indent = Cm(0)
|
||||
h1pf.right_indent = Cm(0)
|
||||
h1pf.space_before = Mm(0)
|
||||
h1pf.space_after = Mm(10)
|
||||
h1pf.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
||||
h1pf.page_break_before = True
|
||||
h1pf.keep_with_next = True
|
||||
h1pf.widow_control = True
|
||||
|
||||
# H2 / H3 — подразделы: абзацный отступ, не caps
|
||||
for level, size, before in ((2, 14, Mm(15)), (3, 14, Mm(10))):
|
||||
style = document.styles[f"Heading {level}"]
|
||||
_set_run_font(style, "Times New Roman", size, bold=True)
|
||||
style.font.all_caps = False
|
||||
hpf = style.paragraph_format
|
||||
hpf.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
|
||||
hpf.first_line_indent = Cm(1.25)
|
||||
hpf.left_indent = Cm(0)
|
||||
hpf.right_indent = Cm(0)
|
||||
hpf.space_before = before
|
||||
hpf.space_after = Mm(10)
|
||||
hpf.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
||||
hpf.page_break_before = False
|
||||
hpf.keep_with_next = True
|
||||
hpf.widow_control = True
|
||||
|
||||
_apply_common_captions_and_misc(document)
|
||||
page = config.page.require_complete()
|
||||
pg.MARGIN_LEFT = Mm(page.left_mm)
|
||||
pg.MARGIN_RIGHT = Mm(page.right_mm)
|
||||
pg.MARGIN_TOP = Mm(page.top_mm)
|
||||
pg.MARGIN_BOTTOM = Mm(page.bottom_mm)
|
||||
|
||||
|
||||
def apply_document_styles(document: Document, style_preset: str = "mirea") -> None:
|
||||
if style_preset == "pis_custom":
|
||||
apply_pis_custom_styles(document)
|
||||
else:
|
||||
apply_mirea_styles(document)
|
||||
|
||||
|
||||
def _apply_common_page_and_body(document: Document) -> None:
|
||||
def apply_style_config(document: Document, config: StyleConfig) -> None:
|
||||
"""Apply full StyleConfig (page margins + paragraph styles) to document."""
|
||||
from .page_geometry import apply_section_geometry, is_landscape_section
|
||||
|
||||
_apply_page_margins(config)
|
||||
|
||||
for section in document.sections:
|
||||
# Do not wipe landscape sections created for +landscape figures/tables.
|
||||
apply_section_geometry(section, landscape=is_landscape_section(section))
|
||||
|
||||
_fix_toc_tab_stops(document)
|
||||
|
||||
normal: ParagraphStyle = document.styles["Normal"]
|
||||
_set_run_font(normal, "Times New Roman", 14)
|
||||
pf = normal.paragraph_format
|
||||
pf.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
|
||||
pf.first_line_indent = Cm(1.25)
|
||||
pf.left_indent = Cm(0)
|
||||
pf.right_indent = Cm(0)
|
||||
pf.space_before = Pt(0)
|
||||
pf.space_after = Pt(0)
|
||||
pf.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
||||
pf.widow_control = True
|
||||
|
||||
|
||||
def _apply_common_captions_and_misc(document: Document) -> None:
|
||||
# --- Caption Figure: 12pt bold, center, under figure ---
|
||||
caption_fig = _ensure_style(document, "Caption Figure", "Caption")
|
||||
_set_run_font(caption_fig, "Times New Roman", 12, bold=True)
|
||||
cpf = caption_fig.paragraph_format
|
||||
cpf.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
cpf.first_line_indent = Cm(0)
|
||||
cpf.left_indent = Cm(0)
|
||||
cpf.space_before = Mm(0)
|
||||
cpf.space_after = Mm(6)
|
||||
cpf.line_spacing_rule = WD_LINE_SPACING.SINGLE
|
||||
cpf.widow_control = True
|
||||
|
||||
# --- Caption Table (legacy EN name) + «Название таблицы» (основной) ---
|
||||
caption_tbl = _ensure_style(document, "Caption Table", "Caption")
|
||||
_set_run_font(caption_tbl, "Times New Roman", 12, italic=True)
|
||||
tpf = caption_tbl.paragraph_format
|
||||
tpf.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
tpf.first_line_indent = Cm(0)
|
||||
tpf.left_indent = Cm(0)
|
||||
tpf.space_before = Mm(6)
|
||||
tpf.space_after = Mm(0)
|
||||
tpf.line_spacing_rule = WD_LINE_SPACING.SINGLE
|
||||
tpf.keep_with_next = True
|
||||
tpf.widow_control = True
|
||||
|
||||
# ГОСТ-имя стиля подписи таблицы (те же параметры, что Caption Table)
|
||||
caption_tbl_ru = _ensure_style(document, "Название таблицы", "Caption Table")
|
||||
_set_run_font(caption_tbl_ru, "Times New Roman", 12, italic=True)
|
||||
tpf_ru = caption_tbl_ru.paragraph_format
|
||||
tpf_ru.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
tpf_ru.first_line_indent = Cm(0)
|
||||
tpf_ru.left_indent = Cm(0)
|
||||
tpf_ru.space_before = Mm(6)
|
||||
tpf_ru.space_after = Mm(0)
|
||||
tpf_ru.line_spacing_rule = WD_LINE_SPACING.SINGLE
|
||||
tpf_ru.keep_with_next = True
|
||||
tpf_ru.widow_control = True
|
||||
|
||||
# --- Caption Listing (как таблицы) ---
|
||||
caption_lst = _ensure_style(document, "Caption Listing", "Caption")
|
||||
_set_run_font(caption_lst, "Times New Roman", 12, italic=True)
|
||||
lpf = caption_lst.paragraph_format
|
||||
lpf.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
lpf.first_line_indent = Cm(0)
|
||||
lpf.left_indent = Cm(0)
|
||||
lpf.space_before = Mm(6)
|
||||
lpf.space_after = Mm(0)
|
||||
lpf.line_spacing_rule = WD_LINE_SPACING.SINGLE
|
||||
lpf.keep_with_next = True
|
||||
lpf.widow_control = True
|
||||
|
||||
caption = document.styles["Caption"]
|
||||
_set_run_font(caption, "Times New Roman", 12, bold=True)
|
||||
capf = caption.paragraph_format
|
||||
capf.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
capf.first_line_indent = Cm(0)
|
||||
capf.space_before = Mm(0)
|
||||
capf.space_after = Mm(6)
|
||||
capf.line_spacing_rule = WD_LINE_SPACING.SINGLE
|
||||
|
||||
code = _ensure_style(document, "Code", "Normal")
|
||||
_set_run_font(code, "Courier New", 10)
|
||||
cdpf = code.paragraph_format
|
||||
cdpf.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
cdpf.first_line_indent = Cm(0)
|
||||
cdpf.left_indent = Cm(0)
|
||||
cdpf.space_before = Mm(0)
|
||||
cdpf.space_after = Mm(0)
|
||||
cdpf.line_spacing_rule = WD_LINE_SPACING.SINGLE
|
||||
|
||||
table_text = _ensure_style(document, "Table Text", "Normal")
|
||||
_set_run_font(table_text, "Times New Roman", 12)
|
||||
ttf = table_text.paragraph_format
|
||||
ttf.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
ttf.first_line_indent = Cm(0)
|
||||
ttf.left_indent = Cm(0)
|
||||
ttf.space_before = Mm(0)
|
||||
ttf.space_after = Mm(0)
|
||||
ttf.line_spacing_rule = WD_LINE_SPACING.SINGLE
|
||||
|
||||
biblio = _ensure_style(document, "Bibliography", "Normal")
|
||||
_set_run_font(biblio, "Times New Roman", 14)
|
||||
bpf = biblio.paragraph_format
|
||||
bpf.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
|
||||
bpf.first_line_indent = Cm(1.25)
|
||||
bpf.space_before = Pt(0)
|
||||
bpf.space_after = Pt(0)
|
||||
bpf.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
||||
|
||||
biblio_h = _ensure_style(document, "Bibliography Heading", "Normal")
|
||||
_set_run_font(biblio_h, "Times New Roman", 14)
|
||||
biblio_h.font.all_caps = True
|
||||
bhpf = biblio_h.paragraph_format
|
||||
bhpf.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
bhpf.first_line_indent = Cm(0)
|
||||
bhpf.left_indent = Cm(1.25) # табл. 5.1
|
||||
bhpf.space_before = Mm(6)
|
||||
bhpf.space_after = Mm(6)
|
||||
bhpf.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
||||
bhpf.keep_with_next = True
|
||||
|
||||
# TOC styles: TNR 14, 1.5, no bold, no first-line indent; toc 1 = ALL CAPS
|
||||
for toc_name, all_caps in (("toc 1", True), ("toc 2", False), ("toc 3", False)):
|
||||
try:
|
||||
toc_style = document.styles[toc_name]
|
||||
except KeyError:
|
||||
toc_style = _ensure_style(document, toc_name, "Normal")
|
||||
_set_run_font(toc_style, "Times New Roman", 14, bold=False)
|
||||
toc_style.font.all_caps = all_caps
|
||||
toc_style.font.bold = False
|
||||
tpf_toc = toc_style.paragraph_format
|
||||
tpf_toc.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
tpf_toc.first_line_indent = Cm(0)
|
||||
tpf_toc.space_before = Pt(0)
|
||||
tpf_toc.space_after = Pt(0)
|
||||
tpf_toc.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
||||
|
||||
try:
|
||||
footer_style = document.styles["Footer"]
|
||||
_set_run_font(footer_style, "Times New Roman", 12)
|
||||
footer_style.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
footer_style.paragraph_format.first_line_indent = Cm(0)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
for hyper_name in ("Hyperlink", "FollowedHyperlink"):
|
||||
try:
|
||||
hyper = document.styles[hyper_name]
|
||||
except KeyError:
|
||||
# Apply in a stable order: Normal first, then headings, then the rest
|
||||
order = [
|
||||
"Normal",
|
||||
"Heading 1", "Heading 2", "Heading 3",
|
||||
"Caption Figure", "Caption Table", "Название таблицы", "Caption Listing", "Caption",
|
||||
"Code", "Table Text",
|
||||
"Bibliography", "Bibliography Heading",
|
||||
"toc 1", "toc 2", "toc 3",
|
||||
"Footer", "Hyperlink", "FollowedHyperlink",
|
||||
"Space After Table",
|
||||
]
|
||||
applied = set()
|
||||
for name in order:
|
||||
spec = config.styles.get(name)
|
||||
if spec is None:
|
||||
continue
|
||||
_set_run_font(hyper, "Times New Roman", 14)
|
||||
hyper.font.underline = False
|
||||
style = _get_or_ensure(document, name)
|
||||
if style is None:
|
||||
continue
|
||||
_apply_paragraph_spec(style, spec)
|
||||
applied.add(name)
|
||||
|
||||
after = _ensure_style(document, "Space After Table", "Normal")
|
||||
apf = after.paragraph_format
|
||||
apf.space_before = Mm(6)
|
||||
apf.space_after = Pt(0)
|
||||
apf.first_line_indent = Cm(1.25)
|
||||
apf.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
||||
for name, spec in config.styles.items():
|
||||
if name in applied:
|
||||
continue
|
||||
style = _get_or_ensure(document, name)
|
||||
if style is None:
|
||||
continue
|
||||
_apply_paragraph_spec(style, spec)
|
||||
|
||||
|
||||
def apply_mirea_styles(document: Document) -> None:
|
||||
"""Mutate section margins and key paragraph styles to match the MIREA method guide."""
|
||||
apply_style_config(document, get_preset("mirea"))
|
||||
|
||||
|
||||
def apply_pis_custom_styles(document: Document) -> None:
|
||||
"""Styles for PIS_custom: итоговый отчёт по практическим работам."""
|
||||
apply_style_config(document, get_preset("pis_custom"))
|
||||
|
||||
|
||||
def apply_document_styles(
|
||||
document: Document,
|
||||
style_preset: str = "mirea",
|
||||
*,
|
||||
overlay: StyleConfig | None = None,
|
||||
md_dir: str | None = None,
|
||||
styles_path: str | None = None,
|
||||
) -> StyleConfig:
|
||||
"""Apply preset (+ optional JSON overlays). Returns the resolved StyleConfig."""
|
||||
if overlay is not None:
|
||||
config = get_preset(style_preset).merge(overlay)
|
||||
else:
|
||||
config = resolve_style_config(
|
||||
style_preset,
|
||||
md_dir=md_dir,
|
||||
styles_path=styles_path,
|
||||
)
|
||||
apply_style_config(document, config)
|
||||
return config
|
||||
|
||||
Reference in New Issue
Block a user