- update документация - промт для ии полу конфигурируемый
This commit is contained in:
+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