Files
Igor20264 510f7e7adf
Python application / build (push) Waiting to run
v0.5.2
Что то сделал
2026-09-08 19:37:54 +03:00

225 lines
7.8 KiB
Python
Raw Permalink 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 ---
# ```uml / ```mermaid / ```uml-c4 … — кавычки и тире там синтаксис
_FENCE_RE = re.compile(
r"^(?P<fence>`{3,}|~{3,})(?P<info>[^\n]*)\n"
r"(?P<body>[\s\S]*?)"
r"^(?P=fence)[ \t]*(?:\n|$)",
re.M,
)
def _fence_lang(info: str) -> str:
return (info.strip().split() or [""])[0].lower()
def _is_diagram_fence_info(info: str) -> bool:
from .diagram_schemes import is_diagram_lang
return is_diagram_lang(_fence_lang(info))
def _map_outside_diagram_fences(text: str, fn) -> str:
"""Apply fn only outside UML / Mermaid / scheme fences."""
out: list[str] = []
pos = 0
for m in _FENCE_RE.finditer(text):
if not _is_diagram_fence_info(m.group("info")):
continue
out.append(fn(text[pos:m.start()]))
out.append(m.group(0))
pos = m.end()
out.append(fn(text[pos:]))
return "".join(out)
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)
def _apply(chunk: str) -> str:
chunk = fix_russian_quotes(chunk)
if _EMDASH_TO_HYPHEN:
return replace_emdash_with_hyphen(chunk)
return fix_dashes(chunk)
text = _map_outside_diagram_fences(text, _apply)
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