update 0.4.4
Python application / build (push) Has been cancelled

- update документация
- промт для ии полу конфигурируемый
This commit is contained in:
Igor20264
2026-09-06 11:04:01 +03:00
parent 638fd38d7f
commit 818a044aa1
26 changed files with 1987 additions and 457 deletions
+156 -230
View File
@@ -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