+144
-17
@@ -9,7 +9,7 @@ import subprocess
|
||||
import traceback
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from getpass import getuser
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
from docx import Document
|
||||
@@ -41,6 +41,10 @@ class ConvertRequest:
|
||||
hr_pagebreak: bool = False
|
||||
title: str | None = None
|
||||
assignment: str | None = None
|
||||
student: str | None = None
|
||||
group: str | None = None
|
||||
# Auto-build title from info_conv.yaml / ```title (off by default).
|
||||
auto_title: bool = False
|
||||
check: bool = False
|
||||
check_only: bool = False
|
||||
strict: bool = False
|
||||
@@ -58,6 +62,15 @@ class ConvertRequest:
|
||||
table_repeat_header: bool = False
|
||||
# PAGE field start on body section; None = continuous from document start.
|
||||
page_number_start: int | None = None
|
||||
# DOCX File → Info; None = take from md2gost.user.json (see doc_metadata).
|
||||
doc_author_source: str | None = None
|
||||
doc_author: str | None = None
|
||||
doc_last_modified_by: str | None = None
|
||||
doc_title: str | None = None
|
||||
doc_subject: str | None = None
|
||||
doc_keywords: str | None = None
|
||||
doc_comments: str | None = None
|
||||
doc_category: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -127,16 +140,33 @@ def _apply_page_offset(document, req: ConvertRequest, *, had_title: bool, had_as
|
||||
from .page_geometry import apply_page_number_start
|
||||
|
||||
start = getattr(req, "page_number_start", None)
|
||||
start_i: int | None
|
||||
if start is None:
|
||||
return
|
||||
try:
|
||||
start_i = int(start)
|
||||
except (TypeError, ValueError):
|
||||
return
|
||||
start_i = None
|
||||
else:
|
||||
try:
|
||||
start_i = int(start)
|
||||
except (TypeError, ValueError):
|
||||
start_i = None
|
||||
front = int(bool(had_title)) + int(bool(had_assignment))
|
||||
# Always strip copied w:pgNumType/@w:start (landscape sections); optional body restart.
|
||||
apply_page_number_start(document, start_i, front_sections=front)
|
||||
|
||||
|
||||
def _page_offset_args(req: ConvertRequest, *, had_title: bool, had_assignment: bool) -> tuple[int | None, int]:
|
||||
"""Return (page_number_start, front_sections) for post-Word patch."""
|
||||
start = getattr(req, "page_number_start", None)
|
||||
if start is None:
|
||||
start_i = None
|
||||
else:
|
||||
try:
|
||||
start_i = int(start)
|
||||
except (TypeError, ValueError):
|
||||
start_i = None
|
||||
front = int(bool(had_title)) + int(bool(had_assignment))
|
||||
return start_i, front
|
||||
|
||||
|
||||
def convert(req: ConvertRequest, log: LogFn | None = None) -> ConvertResult:
|
||||
"""Run check and/or conversion. Does not re-raise; errors go into ConvertResult."""
|
||||
emit: LogFn = log if log is not None else print
|
||||
@@ -154,6 +184,24 @@ def convert(req: ConvertRequest, log: LogFn | None = None) -> ConvertResult:
|
||||
emit(report)
|
||||
return ConvertResult(True, 0, check_report=report, message=report)
|
||||
|
||||
# DOCX → Markdown (word2md) when input is .docx without --check-pages
|
||||
if filename.lower().endswith(".docx"):
|
||||
from word2md import ImportRequest, convert_docx
|
||||
|
||||
out = (req.output or "").strip() or None
|
||||
if out and out.lower().endswith(".docx"):
|
||||
out = str(Path(filename).with_suffix(".md"))
|
||||
result = convert_docx(
|
||||
ImportRequest(filename=filename, output=out),
|
||||
log=emit,
|
||||
)
|
||||
return ConvertResult(
|
||||
result.ok,
|
||||
result.exit_code,
|
||||
output_path=result.output_path,
|
||||
message=result.message,
|
||||
)
|
||||
|
||||
if not filename:
|
||||
return fail(2, "Укажите исходный markdown-файл")
|
||||
if not filename.lower().endswith(".md"):
|
||||
@@ -192,10 +240,57 @@ def convert(req: ConvertRequest, log: LogFn | None = None) -> ConvertResult:
|
||||
with open(filename, encoding="utf-8") as f:
|
||||
md_text = f.read()
|
||||
|
||||
from .title_page import (
|
||||
build_title_docx,
|
||||
load_info_conv,
|
||||
merge_title_meta,
|
||||
parse_and_strip_title_fence,
|
||||
should_generate_title,
|
||||
)
|
||||
from .user_profile import load_user_profile
|
||||
|
||||
fence_fields, body_md = parse_and_strip_title_fence(md_text)
|
||||
yaml_fields = load_info_conv(md_dir)
|
||||
generated_title_path: str | None = None
|
||||
title_path = (req.title or "").strip() or None
|
||||
|
||||
if not output:
|
||||
output = default_output_path(filename)
|
||||
|
||||
if should_generate_title(
|
||||
explicit_title=title_path,
|
||||
fence_fields=fence_fields,
|
||||
md_dir=md_dir,
|
||||
enabled=bool(req.auto_title),
|
||||
):
|
||||
profile = load_user_profile()
|
||||
student = (req.student or "").strip() or profile.student
|
||||
group = (req.group or "").strip() or profile.group
|
||||
if not student or not group:
|
||||
return fail(
|
||||
2,
|
||||
"Для генерации титула нужны ФИО и группа студента "
|
||||
"(GUI: Настройки → Студент…, или CLI: --student / --group).",
|
||||
)
|
||||
meta = merge_title_meta(
|
||||
yaml_fields=yaml_fields,
|
||||
fence_fields=fence_fields,
|
||||
student=student,
|
||||
group=group,
|
||||
)
|
||||
out_dir = os.path.dirname(os.path.abspath(output)) or md_dir
|
||||
generated_title_path = os.path.join(out_dir, ".md2gost_title.docx")
|
||||
try:
|
||||
build_title_docx(meta, generated_title_path)
|
||||
title_path = generated_title_path
|
||||
emit(f"Титульный лист: {generated_title_path}")
|
||||
except Exception as exc:
|
||||
return fail(1, f"Не удалось сгенерировать титул: {exc}")
|
||||
|
||||
check_report = ""
|
||||
if req.check or req.check_only:
|
||||
issues = check_markdown(
|
||||
md_text,
|
||||
body_md,
|
||||
req.doc_type,
|
||||
table_continuation=req.table_continuation,
|
||||
listing_continuation=req.listing_continuation,
|
||||
@@ -211,9 +306,6 @@ def convert(req: ConvertRequest, log: LogFn | None = None) -> ConvertResult:
|
||||
message=check_report,
|
||||
)
|
||||
|
||||
if not output:
|
||||
output = default_output_path(filename)
|
||||
|
||||
template = (req.template or "").strip() or default_template_path()
|
||||
|
||||
handler = _CallbackLogHandler(emit)
|
||||
@@ -236,11 +328,12 @@ def convert(req: ConvertRequest, log: LogFn | None = None) -> ConvertResult:
|
||||
listing_continuation=req.listing_continuation,
|
||||
hr_pagebreak=req.hr_pagebreak,
|
||||
styles_path=req.styles_path or None,
|
||||
markdown_text=body_md,
|
||||
)
|
||||
converter.convert()
|
||||
document = converter.document
|
||||
|
||||
if req.title or req.assignment:
|
||||
if title_path or req.assignment:
|
||||
try:
|
||||
from docxcompose.composer import Composer
|
||||
except ImportError:
|
||||
@@ -256,8 +349,8 @@ def convert(req: ConvertRequest, log: LogFn | None = None) -> ConvertResult:
|
||||
body.remove(child)
|
||||
|
||||
composer = Composer(shell)
|
||||
if req.title:
|
||||
composer.append(Document(req.title))
|
||||
if title_path:
|
||||
composer.append(Document(title_path))
|
||||
shell.add_page_break()
|
||||
if req.assignment:
|
||||
composer.append(Document(req.assignment))
|
||||
@@ -267,19 +360,36 @@ def convert(req: ConvertRequest, log: LogFn | None = None) -> ConvertResult:
|
||||
apply_style_config(document, style_cfg)
|
||||
_fix_front_matter_after_compose(
|
||||
document,
|
||||
had_title=bool(req.title),
|
||||
had_title=bool(title_path),
|
||||
had_assignment=bool(req.assignment),
|
||||
)
|
||||
|
||||
_apply_page_offset(
|
||||
document,
|
||||
req,
|
||||
had_title=bool(req.title),
|
||||
had_title=bool(title_path),
|
||||
had_assignment=bool(req.assignment),
|
||||
)
|
||||
|
||||
document.core_properties.author = getuser()
|
||||
document.core_properties.comments = "Создано при помощи md2gost (ТЗ МИРЭА)"
|
||||
from .doc_metadata import apply_document_metadata, resolve_document_metadata
|
||||
from .user_profile import load_user_profile as _load_user_profile
|
||||
|
||||
profile = _load_user_profile()
|
||||
apply_document_metadata(
|
||||
document,
|
||||
resolve_document_metadata(
|
||||
stored=profile.metadata,
|
||||
student=(req.student or "").strip() or profile.student,
|
||||
author_source=req.doc_author_source,
|
||||
author=req.doc_author,
|
||||
last_modified_by=req.doc_last_modified_by,
|
||||
title=req.doc_title,
|
||||
subject=req.doc_subject,
|
||||
keywords=req.doc_keywords,
|
||||
comments=req.doc_comments,
|
||||
category=req.doc_category,
|
||||
),
|
||||
)
|
||||
try:
|
||||
document.save(output)
|
||||
except PermissionError:
|
||||
@@ -297,12 +407,19 @@ def convert(req: ConvertRequest, log: LogFn | None = None) -> ConvertResult:
|
||||
root_log.removeHandler(handler)
|
||||
attached_diag.removeHandler(handler)
|
||||
attached_diag.setLevel(prev_diag_level)
|
||||
if generated_title_path and os.path.isfile(generated_title_path):
|
||||
try:
|
||||
os.remove(generated_title_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
abs_out = os.path.abspath(output)
|
||||
emit(f"Generated document: {abs_out}")
|
||||
|
||||
if req.table_continuation == "word" or req.listing_continuation == "word":
|
||||
from .word_fix import fix_continuations
|
||||
from .page_geometry import patch_docx_page_starts
|
||||
|
||||
fix = fix_continuations(
|
||||
abs_out,
|
||||
tables=(req.table_continuation == "word"),
|
||||
@@ -317,6 +434,16 @@ def convert(req: ConvertRequest, log: LogFn | None = None) -> ConvertResult:
|
||||
check_report = (
|
||||
(check_report + "\n" + fix.message).strip() if check_report else fix.message
|
||||
)
|
||||
# Word Save often rewrites w:pgNumType/@w:start=\"1\" on every section — strip again.
|
||||
start_i, front = _page_offset_args(
|
||||
req,
|
||||
had_title=bool(title_path),
|
||||
had_assignment=bool(req.assignment),
|
||||
)
|
||||
try:
|
||||
patch_docx_page_starts(abs_out, start_i, front_sections=front)
|
||||
except Exception as exc:
|
||||
emit(f"Не удалось восстановить сквозную нумерацию после Word: {exc}")
|
||||
|
||||
if req.check_pages:
|
||||
from .page_fill_check import check_docx_page_fill, format_page_fill_report
|
||||
|
||||
Reference in New Issue
Block a user