@@ -0,0 +1,185 @@
|
||||
"""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)
|
||||
TABLE_CONTINUATION_MODES = ("off", "legacy", "caption", "soft")
|
||||
DEFAULT_TABLE_CONTINUATION = "off"
|
||||
|
||||
# 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
|
||||
Reference in New Issue
Block a user