Files
md_to_gost/md2gost/profiles.py
T
Igor20264 b38661f588
Python application / build (push) Has been cancelled
Update 0.4.0
- Add\Rework UI
- Add Split Table and Listing
- Add Support Customazeble schems
2026-09-04 22:28:39 +03:00

191 lines
6.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Document type profiles and preprocess helpers for MIREA TZ."""
from __future__ import annotations
import re
from dataclasses import dataclass
from datetime import datetime
DOC_TYPES = ("coursework", "practice", "vkr", "PIS_custom", "APID_coursework")
# auto — Word multilevel list numbers headings; strip digits from md text
# manual — digits stay in md (# 1 … / ## 1.1 …); Word auto-numbering disabled
HEADING_NUMBERING_MODES = ("auto", "manual")
DEFAULT_HEADING_NUMBERING = "manual"
# manual — content built by md2gost with page numbers from layout tracker
# native — Word TOC field (обновить поле при открытии в Word)
TOC_MODES = ("manual", "native")
DEFAULT_TOC_MODE = "native"
# off — одна таблица Word, пагинацию делает Word (без автоподписи)
# legacy — режем по оценке высоты + «Продолжение…» с page_break_before
# caption — режем по оценке + явный PageBreak + «Продолжение…»
# soft — синоним off (автоподпись mid-page без точной вёрстки Word невозможна в DOCX)
# word — как off при рендере; после save Word COM режет по реальной пагинации + «Продолжение…»
TABLE_CONTINUATION_MODES = ("off", "legacy", "caption", "soft", "word")
DEFAULT_TABLE_CONTINUATION = "word"
# Same modes as tables (word = post-process via Word COM after save).
LISTING_CONTINUATION_MODES = TABLE_CONTINUATION_MODES
DEFAULT_LISTING_CONTINUATION = DEFAULT_TABLE_CONTINUATION
# section — Рисунок 1.1 / 2.1 (ТЗ МИРЭА); continuous — Рисунок 1, 2, 3 (ПИС)
NUMBERING_SCOPES = ("section", "continuous")
STYLE_PRESETS = ("mirea", "pis_custom")
@dataclass
class DocProfile:
name: str
min_sources: int
max_sources: int | None # None = no upper bound
sectional_biblio: bool
require_graphic_appendix: bool
max_source_age_years: int = 5
# mirea — ГОСТ/методичка МИРЭА; pis_custom — итоговый отчёт по практическим работам
style_preset: str = "mirea"
numbering_scope: str = "section"
# Structure expectations for checker
require_intro_conclusion: bool = True
require_bibliography: bool = True
require_practical_works: bool = False
# КР по «Архитектуре приложений и данных» (методичка Аншиной/Лагуновой)
require_apid_kr_structure: bool = False
PROFILES = {
"coursework": DocProfile("coursework", 5, 7, False, False),
"practice": DocProfile("practice", 5, 7, False, False),
"vkr": DocProfile("vkr", 10, None, True, True),
# Курсовая АПИД: оформление ГОСТ/Кириллина, источники 7–20 по методичке дисциплины
"APID_coursework": DocProfile(
"APID_coursework",
min_sources=7,
max_sources=20,
sectional_biblio=False,
require_graphic_appendix=False,
max_source_age_years=5,
style_preset="mirea",
numbering_scope="section",
require_intro_conclusion=True,
require_bibliography=True,
require_practical_works=False,
require_apid_kr_structure=True,
),
# Итоговый отчёт по практическим работам (чек-лист ПИС)
"PIS_custom": DocProfile(
"PIS_custom",
min_sources=0,
max_sources=None,
sectional_biblio=False,
require_graphic_appendix=False,
max_source_age_years=10,
style_preset="pis_custom",
numbering_scope="continuous",
require_intro_conclusion=False,
require_bibliography=False,
require_practical_works=True,
),
}
def get_profile(doc_type: str) -> DocProfile:
if doc_type not in PROFILES:
raise ValueError(f"Unknown document type: {doc_type}. Expected one of {DOC_TYPES}")
return PROFILES[doc_type]
# --- text autofixes ---
def fix_russian_quotes(text: str) -> str:
"""Replace "..." with «...» for Russian text segments (heuristic)."""
def repl(m):
inner = m.group(1)
if inner.lstrip().startswith("%"):
return m.group(0)
return f"«{inner}»"
return re.sub(r'"([^"\n]+)"', repl, text)
def fix_dashes(text: str) -> str:
"""Replace spaced hyphen used as dash with em dash; keep range hyphens."""
# "слово - слово" → "слово — слово"
text = re.sub(r"(\S) - (\S)", r"\1 — \2", text)
return text
def replace_emdash_with_hyphen(text: str) -> str:
"""Replace typographic em dash U+2014 with ASCII hyphen-minus."""
return text.replace("", "-")
# Runtime switch for — vs - (set from CLI / Converter)
_EMDASH_TO_HYPHEN = False
def set_emdash_to_hyphen(enabled: bool) -> None:
global _EMDASH_TO_HYPHEN
_EMDASH_TO_HYPHEN = bool(enabled)
def emdash_to_hyphen_enabled() -> bool:
return _EMDASH_TO_HYPHEN
def dash_separator() -> str:
"""Caption / prose dash: ' - ' or '' depending on CLI flag."""
return " - " if _EMDASH_TO_HYPHEN else ""
def preprocess_markdown(text: str, emdash_to_hyphen: bool | None = None) -> str:
if emdash_to_hyphen is not None:
set_emdash_to_hyphen(emdash_to_hyphen)
text = fix_russian_quotes(text)
if _EMDASH_TO_HYPHEN:
# Keep ASCII hyphens; flatten any em dashes from the source
text = replace_emdash_with_hyphen(text)
else:
text = fix_dashes(text)
text = separate_biblio_lines(text)
return text
def separate_biblio_lines(text: str) -> str:
"""Ensure each [n]: bibliography line is its own paragraph (Marko soft-breaks otherwise)."""
lines = text.splitlines(keepends=True)
out: list[str] = []
in_biblio = False
for line in lines:
stripped = line.strip()
if re.match(r"^#\s*\*?\s*СПИСОК\s+ИСПОЛЬЗОВАНН?ЫХ\s+ИСТОЧНИКОВ\s*$", stripped, re.I):
in_biblio = True
out.append(line)
continue
if in_biblio and re.match(r"^#\s+", stripped):
in_biblio = False
if in_biblio and re.match(r"^\[\d+(?:\.\d+)?\]:", stripped):
# blank line before entry if previous wasn't blank
if out and out[-1].strip():
out.append("\n")
out.append(line if line.endswith("\n") else line + "\n")
out.append("\n")
continue
out.append(line)
return "".join(out)
def find_formula_refs(text: str) -> set[str]:
"""Labels of formulas that are cited, e.g. @Формула:euler or (1.1) after %eq."""
refs = set()
for m in re.finditer(r"@Формула:(\w+)", text, re.IGNORECASE):
refs.add(m.group(1))
for m in re.finditer(r"@Formula:(\w+)", text, re.IGNORECASE):
refs.add(m.group(1))
return refs
def current_year() -> int:
return datetime.now().year