refactor: 重构项目结构并更新依赖配置
- 移除原有的 docx_thesis 模块及其相关文件 (cli.py, config.py, converter.py) - 新增 .claudeignore 文件以忽略 Python 生成文件和缓存 - 更新 .gitignore 文件添加更多忽略规则包括 .mypy_cache/, .ruff_cache/, .claude/, *.md 等 - 添加 README.md 使用说明文档 - 修改 pyproject.toml 依赖配置,新增 docxtpl、pyyaml, 移除原 thesis 命令入口点并更新为 transit.__main__ - 新增 transit 模块及相应初始化文件 - 重命名 main.py 为快速入口脚本
This commit is contained in:
17
.claudeignore
Normal file
17
.claudeignore
Normal file
@@ -0,0 +1,17 @@
|
||||
# Python-generated files
|
||||
__pycache__/
|
||||
*.py[oc]
|
||||
build/
|
||||
dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
|
||||
.mypy_cache/
|
||||
.ruff_cache/
|
||||
|
||||
*.docx
|
||||
*.doc
|
||||
*.txt
|
||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -8,3 +8,15 @@ wheels/
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
|
||||
.mypy_cache/
|
||||
.ruff_cache/
|
||||
|
||||
.claude/
|
||||
|
||||
*.md
|
||||
!README.md
|
||||
|
||||
*.docx
|
||||
*.doc
|
||||
*.txt
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
from .config import ThesisFormat
|
||||
from .converter import ThesisConverter
|
||||
|
||||
__all__ = ["ThesisFormat", "ThesisConverter"]
|
||||
@@ -1,43 +0,0 @@
|
||||
"""CLI entry point for the thesis converter (used by `uv run thesis`)."""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from .config import ThesisFormat
|
||||
from .converter import ThesisConverter
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="将 Markdown 毕业论文转换为格式化的 Word 文档",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=(
|
||||
"示例:\n"
|
||||
" uv run thesis 论文.md # 输出 论文.docx\n"
|
||||
" uv run thesis 论文.md 毕业设计.docx # 指定输出文件名\n"
|
||||
),
|
||||
)
|
||||
parser.add_argument("input", help="输入的 .md 文件路径")
|
||||
parser.add_argument("output", nargs="?", default=None,
|
||||
help="输出的 .docx 路径(默认自动替换扩展名)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
inp = Path(args.input)
|
||||
if not inp.exists():
|
||||
print(f"错误:找不到文件 {inp}")
|
||||
sys.exit(1)
|
||||
|
||||
out = Path(args.output) if args.output else inp.with_suffix(".docx")
|
||||
|
||||
config = ThesisFormat()
|
||||
converter = ThesisConverter(config)
|
||||
converter.convert(str(inp), str(out))
|
||||
|
||||
print(f"转换完成:{out}")
|
||||
print("提示:在 Word 中打开后,按 Ctrl+A → F9 更新目录和页码。")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,90 +0,0 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class ThesisFormat:
|
||||
"""毕业论文格式配置(桂林理工大学理工类标准)。
|
||||
|
||||
依据《要求.txt》中第2、3节的规定。
|
||||
所有尺寸单位:厘米(cm) 或 磅(pt),按字段说明为准。
|
||||
"""
|
||||
|
||||
# ════════════════════════════════════════════
|
||||
# 页面设置 (§2.2)
|
||||
# ════════════════════════════════════════════
|
||||
page_width: float = 21.0 # A4
|
||||
page_height: float = 29.7 # A4
|
||||
margin_top: float = 3.0 # 上3.0cm
|
||||
margin_bottom: float = 2.5 # 下2.5cm
|
||||
margin_left: float = 3.0 # 左3.0cm
|
||||
margin_right: float = 2.0 # 右2.0cm
|
||||
footer_distance: float = 1.5 # 页脚1.5cm
|
||||
|
||||
# ════════════════════════════════════════════
|
||||
# 字体 (§3.1) — 理工类全文宋体
|
||||
# ════════════════════════════════════════════
|
||||
font_cn: str = "宋体"
|
||||
font_cn_heading: str = "宋体" # 标题也用宋体(加粗区分)
|
||||
font_en: str = "Times New Roman"
|
||||
font_code: str = "Courier New"
|
||||
font_heading_en: str = "Times New Roman"
|
||||
|
||||
# ════════════════════════════════════════════
|
||||
# 字号(磅)(§3.2)
|
||||
# ════════════════════════════════════════════
|
||||
# 中文:三号=16pt 小三=15pt 四号=14pt 小四=12pt 五号=10.5pt 小五=9pt
|
||||
size_abstract_title: float = 16 # 三号 — 摘要题头
|
||||
size_chapter: float = 16 # 三号 — 第一层次(章)
|
||||
size_section: float = 15 # 小三 — 第二层次(节)
|
||||
size_subsection: float = 14 # 四号 — 第三层次(条)
|
||||
size_subsubsection: float = 12 # 小四 — 第四层次(款)
|
||||
size_body: float = 12 # 小四 — 正文
|
||||
size_keyword_label: float = 12 # 小四 — 关键词题头
|
||||
size_reference: float = 10.5 # 五号 — 参考文献条目
|
||||
size_code: float = 9 # 小五 — 代码块
|
||||
size_footer: float = 9 # 小五 — 页脚页码
|
||||
|
||||
# ════════════════════════════════════════════
|
||||
# 段落间距(磅)
|
||||
# ════════════════════════════════════════════
|
||||
spacing_before_chapter: int = 0
|
||||
spacing_after_chapter: int = 0
|
||||
spacing_before_section: int = 0
|
||||
spacing_after_section: int = 0
|
||||
spacing_before_subsection: int = 0
|
||||
spacing_after_subsection: int = 0
|
||||
|
||||
# ════════════════════════════════════════════
|
||||
# 行距 (§2.2) — 单倍行距
|
||||
# ════════════════════════════════════════════
|
||||
line_spacing_body: float = 1.0
|
||||
line_spacing_heading: float = 1.0
|
||||
line_spacing_reference: float = 1.0
|
||||
line_spacing_code: float = 1.0
|
||||
|
||||
# ════════════════════════════════════════════
|
||||
# 缩进
|
||||
# ════════════════════════════════════════════
|
||||
first_line_indent_chars: int = 2 # 正文首行缩进2字符
|
||||
|
||||
# ════════════════════════════════════════════
|
||||
# 文档网格 (§2.2) — 每页32行×每行42字
|
||||
# ════════════════════════════════════════════
|
||||
grid_chars_per_line: int = 42
|
||||
grid_lines_per_page: int = 32
|
||||
|
||||
# ════════════════════════════════════════════
|
||||
# 章节标题文字识别
|
||||
# ════════════════════════════════════════════
|
||||
abstract_title_cn: str = "摘 要"
|
||||
abstract_title_en: str = "Abstract"
|
||||
toc_title: str = "目 次"
|
||||
acknowledgement_title: str = "致谢"
|
||||
references_title: str = "参考文献"
|
||||
appendix_title: str = "附录"
|
||||
|
||||
# ════════════════════════════════════════════
|
||||
# 摘要关键词标记
|
||||
# ════════════════════════════════════════════
|
||||
keywords_label_cn: str = "关键词:"
|
||||
keywords_label_en: str = "Key words:"
|
||||
@@ -1,795 +0,0 @@
|
||||
"""Convert Markdown graduation thesis → formatted Word .docx.
|
||||
|
||||
Parses markdown line-by-line and writes a python-docx document that
|
||||
complies with 桂林理工大学 理工类毕业设计(论文)格式要求.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from docx import Document
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING
|
||||
from docx.oxml import parse_xml
|
||||
from docx.oxml.ns import nsdecls, qn
|
||||
from docx.shared import Cm, Pt, RGBColor
|
||||
from docx.text.paragraph import Paragraph
|
||||
from docx.text.run import Run
|
||||
|
||||
from .config import ThesisFormat
|
||||
|
||||
|
||||
# ── font helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _set_font(
|
||||
run: Run,
|
||||
cn_font: str,
|
||||
en_font: str | None = None,
|
||||
size: float | None = None,
|
||||
bold: bool | None = None,
|
||||
italic: bool | None = None,
|
||||
):
|
||||
if en_font:
|
||||
run.font.name = en_font
|
||||
if cn_font:
|
||||
rpr = run._element.get_or_add_rPr()
|
||||
rfonts = rpr.find(qn("w:rFonts"))
|
||||
if rfonts is None:
|
||||
rfonts = parse_xml(f'<w:rFonts {nsdecls("w")}/>')
|
||||
rpr.insert(0, rfonts)
|
||||
rfonts.set(qn("w:eastAsia"), cn_font)
|
||||
if size is not None:
|
||||
run.font.size = Pt(size)
|
||||
if bold is not None:
|
||||
run.font.bold = bold
|
||||
if italic is not None:
|
||||
run.font.italic = italic
|
||||
|
||||
|
||||
def _set_spacing(p: Paragraph, before: int = 0, after: int = 0,
|
||||
line_spacing: float = 1.0):
|
||||
pf = p.paragraph_format
|
||||
pf.space_before = Pt(before)
|
||||
pf.space_after = Pt(after)
|
||||
pf.line_spacing_rule = WD_LINE_SPACING.MULTIPLE
|
||||
pf.line_spacing = line_spacing
|
||||
|
||||
|
||||
def _set_indent(p: Paragraph, chars: int = 2):
|
||||
if chars > 0:
|
||||
p.paragraph_format.first_line_indent = Cm(chars * 0.37)
|
||||
|
||||
|
||||
def _set_page_number_fmt(section, fmt: str):
|
||||
sect_pr = section._sectPr
|
||||
el = sect_pr.find(qn("w:pgNumType"))
|
||||
if el is None:
|
||||
el = parse_xml(f'<w:pgNumType {nsdecls("w")}/>')
|
||||
sect_pr.append(el)
|
||||
el.set(qn("w:fmt"), fmt)
|
||||
|
||||
|
||||
def _setup_footer(section, roman: bool):
|
||||
footer = section.footer
|
||||
footer.is_linked_to_previous = False
|
||||
|
||||
# clear default empty paragraph runs to avoid extra blank line
|
||||
for p in footer.paragraphs:
|
||||
for r in p.runs:
|
||||
r.text = ""
|
||||
|
||||
p = footer.paragraphs[0]
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
p.paragraph_format.space_before = Pt(0)
|
||||
p.paragraph_format.space_after = Pt(0)
|
||||
|
||||
r = p.add_run()
|
||||
_set_font(r, "宋体", "Times New Roman", size=9)
|
||||
r._element.append(parse_xml(
|
||||
f'<w:fldChar {nsdecls("w")} w:fldCharType="begin"/>'))
|
||||
r2 = p.add_run()
|
||||
r2._element.append(parse_xml(
|
||||
f'<w:instrText {nsdecls("w")} xml:space="preserve"> PAGE </w:instrText>'))
|
||||
r3 = p.add_run()
|
||||
r3._element.append(parse_xml(
|
||||
f'<w:fldChar {nsdecls("w")} w:fldCharType="end"/>'))
|
||||
|
||||
_set_page_number_fmt(section, "lowerRoman" if roman else "decimal")
|
||||
|
||||
|
||||
# ── inline markdown parser ───────────────────────────────────────────────
|
||||
|
||||
|
||||
def _parse_inline(text: str):
|
||||
"""Tokenise line → list of (text, attrs) tuples."""
|
||||
tokens: list[tuple[str, dict]] = []
|
||||
buf = ""
|
||||
i = 0
|
||||
n = len(text)
|
||||
|
||||
def flush():
|
||||
nonlocal buf
|
||||
if buf:
|
||||
tokens.append((buf, {}))
|
||||
buf = ""
|
||||
|
||||
while i < n:
|
||||
ch = text[i]
|
||||
# `code`
|
||||
if ch == "`":
|
||||
flush()
|
||||
j = text.find("`", i + 1)
|
||||
if j == -1:
|
||||
buf += ch
|
||||
i += 1
|
||||
continue
|
||||
tokens.append((text[i + 1:j], {"code": True}))
|
||||
i = j + 1
|
||||
continue
|
||||
# **bold**
|
||||
if text[i:i + 2] == "**":
|
||||
flush()
|
||||
j = text.find("**", i + 2)
|
||||
if j == -1:
|
||||
buf += ch
|
||||
i += 1
|
||||
continue
|
||||
inner = text[i + 2:j]
|
||||
sub = _parse_inline(inner)
|
||||
for t, a in sub:
|
||||
a["bold"] = True
|
||||
tokens.append((t, a))
|
||||
i = j + 2
|
||||
continue
|
||||
# *italic* (single star, not **)
|
||||
if ch == "*" and i + 1 < n and text[i + 1] != "*":
|
||||
flush()
|
||||
j = text.find("*", i + 1)
|
||||
if j == -1:
|
||||
buf += ch
|
||||
i += 1
|
||||
continue
|
||||
tokens.append((text[i + 1:j], {"italic": True}))
|
||||
i = j + 1
|
||||
continue
|
||||
buf += ch
|
||||
i += 1
|
||||
flush()
|
||||
return tokens
|
||||
|
||||
|
||||
def _add_inline(p: Paragraph, tokens: list, cfg: ThesisFormat,
|
||||
size: float | None = None, bold: bool = False):
|
||||
for text, attrs in tokens:
|
||||
run = p.add_run(text)
|
||||
b = bold or attrs.get("bold", False)
|
||||
it = attrs.get("italic", False)
|
||||
code = attrs.get("code", False)
|
||||
cn = cfg.font_code if code else cfg.font_cn
|
||||
en = cfg.font_code if code else cfg.font_en
|
||||
_set_font(run, cn, en, size=size or cfg.size_body,
|
||||
bold=b, italic=it if not b else None)
|
||||
|
||||
|
||||
# ── block-level parser ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _parse_blocks(text: str):
|
||||
lines = text.split("\n")
|
||||
blocks: list[dict] = []
|
||||
i, n = 0, len(lines)
|
||||
|
||||
while i < n:
|
||||
line = lines[i]
|
||||
|
||||
# thematic break
|
||||
if line.strip() == "---":
|
||||
blocks.append({"type": "thematic_break"})
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# fenced code block
|
||||
if line.strip().startswith("```") or line.strip().startswith("~~~"):
|
||||
fence = line.strip()[:3]
|
||||
info = line.strip()[3:].strip()
|
||||
code_lines: list[str] = []
|
||||
i += 1
|
||||
while i < n and not lines[i].strip().startswith(fence):
|
||||
code_lines.append(lines[i])
|
||||
i += 1
|
||||
i += 1
|
||||
blocks.append({"type": "block_code", "info": info,
|
||||
"raw": "\n".join(code_lines)})
|
||||
continue
|
||||
|
||||
# heading
|
||||
m = re.match(r"^(#{1,6})\s+(.+)$", line)
|
||||
if m:
|
||||
blocks.append({"type": "heading",
|
||||
"level": len(m.group(1)),
|
||||
"text": m.group(2).strip()})
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# blockquote
|
||||
if line.strip().startswith(">"):
|
||||
ql: list[str] = []
|
||||
while i < n and (lines[i].strip().startswith(">")
|
||||
or lines[i].strip() == ""):
|
||||
ql.append(re.sub(r"^>\s?", "", lines[i]))
|
||||
i += 1
|
||||
blocks.append({"type": "block_quote",
|
||||
"text": "\n".join(ql).strip()})
|
||||
continue
|
||||
|
||||
# list
|
||||
if re.match(r"^(\s*)([-*+]\s|\d+\.\s)", line):
|
||||
items: list[str] = []
|
||||
while i < n:
|
||||
if re.match(r"^(\s*)([-*+]\s|\d+\.\s)", lines[i]):
|
||||
t = re.sub(r"^(\s*)[-*+]\s|\d+\.\s", "", lines[i], 1)
|
||||
items.append(t)
|
||||
i += 1
|
||||
while i < n and lines[i].strip() \
|
||||
and not re.match(r"^(\s*)([-*+]\s|\d+\.\s)",
|
||||
lines[i]):
|
||||
if lines[i][0] in " \t":
|
||||
items[-1] += " " + lines[i].strip()
|
||||
i += 1
|
||||
else:
|
||||
break
|
||||
elif lines[i].strip() == "":
|
||||
i += 1
|
||||
else:
|
||||
break
|
||||
blocks.append({"type": "list", "items": items})
|
||||
continue
|
||||
|
||||
# blank
|
||||
if line.strip() == "":
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# paragraph (accumulate)
|
||||
para: list[str] = []
|
||||
while i < n and lines[i].strip():
|
||||
para.append(lines[i])
|
||||
i += 1
|
||||
t = "\n".join(para).strip()
|
||||
if t:
|
||||
blocks.append({"type": "paragraph", "text": t})
|
||||
|
||||
return blocks
|
||||
|
||||
|
||||
# ── converter ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class ThesisConverter:
|
||||
"""Markdown → 理工类毕业论文 Word 文档。
|
||||
|
||||
处理流程:
|
||||
1. 解析 MD → blocks
|
||||
2. 扫描 blocks 提取论文题目(H1)
|
||||
3. 按章节类别写入带正确格式的 Word
|
||||
4. 每章自动分页、页面网格、字体字号严格按学校要求
|
||||
"""
|
||||
|
||||
def __init__(self, config: ThesisFormat | None = None):
|
||||
self.config = config or ThesisFormat()
|
||||
self.doc = Document()
|
||||
self._thesis_title: str = "" # 论文题目(来自 H1)
|
||||
self._has_title = False # 是否已保存论文题目
|
||||
self._section_break_added = False # 是否插入过正文分节符
|
||||
|
||||
# ── public API ──────────────────────────────────────────────────
|
||||
|
||||
def convert(self, md_path: str | Path, docx_path: str | Path):
|
||||
text = Path(md_path).read_text(encoding="utf-8")
|
||||
text = self._strip_manual_toc(text)
|
||||
blocks = _parse_blocks(text)
|
||||
|
||||
# extract H1 thesis title
|
||||
for blk in blocks:
|
||||
if blk["type"] == "heading" and blk["level"] == 1:
|
||||
self._thesis_title = blk["text"]
|
||||
break
|
||||
|
||||
self._setup_document()
|
||||
self._process_blocks(blocks)
|
||||
self.doc.save(str(docx_path))
|
||||
|
||||
# ── strip manual TOC ────────────────────────────────────────────
|
||||
|
||||
@staticmethod
|
||||
def _strip_manual_toc(text: str) -> str:
|
||||
lines = text.split("\n")
|
||||
toc_start = -1
|
||||
sep_end = -1
|
||||
for i, line in enumerate(lines):
|
||||
if re.search(r"[目目]\s*[次次]", line) and line.startswith("#"):
|
||||
toc_start = i
|
||||
if toc_start >= 0 and line.strip() == "---" and i > toc_start:
|
||||
sep_end = i
|
||||
break
|
||||
if toc_start >= 0 and sep_end > toc_start:
|
||||
kept = lines[:toc_start + 1]
|
||||
kept.append("")
|
||||
kept.extend(lines[sep_end:])
|
||||
return "\n".join(kept)
|
||||
return text
|
||||
|
||||
# ── page setup ──────────────────────────────────────────────────
|
||||
|
||||
def _setup_document(self):
|
||||
cfg = self.config
|
||||
sec = self.doc.sections[0]
|
||||
self._apply_page_setup(sec, roman=True)
|
||||
|
||||
# default font
|
||||
styles = self.doc.styles
|
||||
normal = styles["Normal"]
|
||||
rpr = normal.element.get_or_add_rPr()
|
||||
rfonts = rpr.find(qn("w:rFonts"))
|
||||
if rfonts is None:
|
||||
rfonts = parse_xml(f'<w:rFonts {nsdecls("w")}/>')
|
||||
rpr.insert(0, rfonts)
|
||||
rfonts.set(qn("w:ascii"), cfg.font_en)
|
||||
rfonts.set(qn("w:hAnsi"), cfg.font_en)
|
||||
rfonts.set(qn("w:eastAsia"), cfg.font_cn)
|
||||
rfonts.set(qn("w:cs"), cfg.font_en)
|
||||
|
||||
sz = rpr.find(qn("w:sz"))
|
||||
if sz is None:
|
||||
sz = parse_xml(
|
||||
f'<w:sz {nsdecls("w")} w:val="{int(cfg.size_body * 2)}"/>')
|
||||
rpr.append(sz)
|
||||
pf = normal.paragraph_format
|
||||
pf.line_spacing_rule = WD_LINE_SPACING.MULTIPLE
|
||||
pf.line_spacing = cfg.line_spacing_body
|
||||
|
||||
self._config_heading_styles()
|
||||
|
||||
_setup_footer(sec, roman=True)
|
||||
|
||||
def _config_heading_styles(self):
|
||||
"""Configure Heading 1/2/3 built-in styles to match thesis formatting.
|
||||
|
||||
This ensures Word's TOC field can detect headings and auto-generate
|
||||
the table of contents correctly.
|
||||
"""
|
||||
cfg = self.config
|
||||
styles = self.doc.styles
|
||||
|
||||
# ── Heading 1 = 章 (三号宋体加粗左) ──────────────────────────
|
||||
h1 = styles["Heading 1"]
|
||||
h1.font.name = cfg.font_heading_en
|
||||
rpr = h1.element.get_or_add_rPr()
|
||||
rfonts = rpr.find(qn("w:rFonts"))
|
||||
if rfonts is None:
|
||||
rfonts = parse_xml(f'<w:rFonts {nsdecls("w")}/>')
|
||||
rpr.insert(0, rfonts)
|
||||
rfonts.set(qn("w:eastAsia"), cfg.font_cn_heading)
|
||||
h1.font.size = Pt(cfg.size_chapter)
|
||||
h1.font.bold = True
|
||||
h1.font.color.rgb = RGBColor(0, 0, 0)
|
||||
h1.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
h1.paragraph_format.line_spacing_rule = WD_LINE_SPACING.MULTIPLE
|
||||
h1.paragraph_format.line_spacing = cfg.line_spacing_heading
|
||||
h1.paragraph_format.space_before = Pt(0)
|
||||
h1.paragraph_format.space_after = Pt(0)
|
||||
# Keep with next + page break before
|
||||
pPr = h1.element.get_or_add_pPr()
|
||||
keep_next = parse_xml(f'<w:keepNext {nsdecls("w")}/>')
|
||||
pPr.append(keep_next)
|
||||
|
||||
# ── Heading 2 = 节 (小三号宋体加粗左) ────────────────────────
|
||||
h2 = styles["Heading 2"]
|
||||
h2.font.name = cfg.font_heading_en
|
||||
rpr2 = h2.element.get_or_add_rPr()
|
||||
rfonts2 = rpr2.find(qn("w:rFonts"))
|
||||
if rfonts2 is None:
|
||||
rfonts2 = parse_xml(f'<w:rFonts {nsdecls("w")}/>')
|
||||
rpr2.insert(0, rfonts2)
|
||||
rfonts2.set(qn("w:eastAsia"), cfg.font_cn_heading)
|
||||
h2.font.size = Pt(cfg.size_section)
|
||||
h2.font.bold = True
|
||||
h2.font.color.rgb = RGBColor(0, 0, 0)
|
||||
h2.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
h2.paragraph_format.line_spacing_rule = WD_LINE_SPACING.MULTIPLE
|
||||
h2.paragraph_format.line_spacing = cfg.line_spacing_heading
|
||||
h2.paragraph_format.space_before = Pt(0)
|
||||
h2.paragraph_format.space_after = Pt(0)
|
||||
|
||||
# ── Heading 3 = 条 (四号宋体加粗左) ──────────────────────────
|
||||
h3 = styles["Heading 3"]
|
||||
h3.font.name = cfg.font_heading_en
|
||||
rpr3 = h3.element.get_or_add_rPr()
|
||||
rfonts3 = rpr3.find(qn("w:rFonts"))
|
||||
if rfonts3 is None:
|
||||
rfonts3 = parse_xml(f'<w:rFonts {nsdecls("w")}/>')
|
||||
rpr3.insert(0, rfonts3)
|
||||
rfonts3.set(qn("w:eastAsia"), cfg.font_cn_heading)
|
||||
h3.font.size = Pt(cfg.size_subsection)
|
||||
h3.font.bold = True
|
||||
h3.font.color.rgb = RGBColor(0, 0, 0)
|
||||
h3.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
h3.paragraph_format.line_spacing_rule = WD_LINE_SPACING.MULTIPLE
|
||||
h3.paragraph_format.line_spacing = cfg.line_spacing_heading
|
||||
h3.paragraph_format.space_before = Pt(0)
|
||||
h3.paragraph_format.space_after = Pt(0)
|
||||
|
||||
def _add_section_break_main(self):
|
||||
sec = self.doc.add_section()
|
||||
self._apply_page_setup(sec, roman=False)
|
||||
self._section_break_added = True
|
||||
|
||||
def _apply_page_setup(self, sec, roman: bool = True):
|
||||
"""Apply margins, grid, and footer to a section."""
|
||||
cfg = self.config
|
||||
sec.page_width = Cm(cfg.page_width)
|
||||
sec.page_height = Cm(cfg.page_height)
|
||||
|
||||
sect_pr = sec._sectPr
|
||||
for el in list(sect_pr):
|
||||
if el.tag in (qn("w:pgMar"), qn("w:docGrid")):
|
||||
sect_pr.remove(el)
|
||||
|
||||
pgMar = parse_xml(
|
||||
f'<w:pgMar {nsdecls("w")} '
|
||||
f'w:top="{int(cfg.margin_top * 567)}" '
|
||||
f'w:bottom="{int(cfg.margin_bottom * 567)}" '
|
||||
f'w:left="{int(cfg.margin_left * 567)}" '
|
||||
f'w:right="{int(cfg.margin_right * 567)}" '
|
||||
f'w:header="0" '
|
||||
f'w:footer="{int(cfg.footer_distance * 567)}"/>')
|
||||
sect_pr.append(pgMar)
|
||||
|
||||
text_height_mm = (cfg.page_height - cfg.margin_top
|
||||
- cfg.margin_bottom) * 10
|
||||
line_pitch = int(text_height_mm / cfg.grid_lines_per_page * 56.7)
|
||||
text_width_mm = (cfg.page_width - cfg.margin_left
|
||||
- cfg.margin_right) * 10
|
||||
char_pitch = int(text_width_mm / cfg.grid_chars_per_line * 56.7)
|
||||
dg = parse_xml(
|
||||
f'<w:docGrid {nsdecls("w")} '
|
||||
f'w:type="linesAndChars" '
|
||||
f'w:linePitch="{line_pitch}" '
|
||||
f'w:charSpace="{char_pitch}"/>')
|
||||
sect_pr.append(dg)
|
||||
|
||||
_setup_footer(sec, roman=roman)
|
||||
|
||||
# ── block processing ────────────────────────────────────────────
|
||||
|
||||
def _process_blocks(self, blocks):
|
||||
# State machine:
|
||||
# before_abstract → abstract_cn → abstract_en → toc → main
|
||||
state = "before_abstract"
|
||||
self._seen_first_chapter = False
|
||||
|
||||
for blk in blocks:
|
||||
t = blk["type"]
|
||||
|
||||
if t == "heading" and blk["level"] == 1:
|
||||
# Skip H1 (thesis title) — not rendered on Chinese abstract
|
||||
continue
|
||||
|
||||
if t == "heading" and blk["level"] == 2:
|
||||
txt = blk["text"].strip()
|
||||
if txt.replace(" ", "") == "摘 要".replace(" ", ""):
|
||||
state = "abstract_cn"
|
||||
self._add_abstract_title("摘 要")
|
||||
continue
|
||||
if txt == "Abstract":
|
||||
self._add_abstract_title_en()
|
||||
state = "abstract_en"
|
||||
continue
|
||||
if "目" in txt and "次" in txt:
|
||||
state = "toc"
|
||||
self._add_toc("目 次")
|
||||
continue
|
||||
# Normal chapter
|
||||
if state in ("before_abstract", "abstract_cn", "abstract_en", "toc"):
|
||||
self._add_section_break_main()
|
||||
state = "main"
|
||||
self._add_page_break_if_not_first()
|
||||
self._add_chapter(txt)
|
||||
continue
|
||||
|
||||
if t == "heading" and blk["level"] == 3:
|
||||
self._ensure_main_section(state)
|
||||
state = "main"
|
||||
txt = blk["text"].strip()
|
||||
if re.match(r"^\d+\.\d+\.\d+\s", txt):
|
||||
self._add_subsection(txt)
|
||||
else:
|
||||
self._add_section(txt)
|
||||
continue
|
||||
|
||||
if t == "heading" and blk["level"] >= 4:
|
||||
self._ensure_main_section(state)
|
||||
state = "main"
|
||||
# headings below 3 → body-style bold
|
||||
self._add_body_para(blk["text"], bold=True, indent=False)
|
||||
continue
|
||||
|
||||
# paragraphs / code / blockquote / list / thematic_break
|
||||
|
||||
if t == "paragraph":
|
||||
txt = blk["text"]
|
||||
if not txt.strip():
|
||||
continue
|
||||
if state == "abstract_cn":
|
||||
if txt.startswith("关键词:"):
|
||||
self._add_keywords(txt, cn=True)
|
||||
else:
|
||||
self._add_abstract_body(txt)
|
||||
continue
|
||||
if state == "abstract_en":
|
||||
if txt.startswith("Key words:"):
|
||||
self._add_keywords(txt, cn=False)
|
||||
else:
|
||||
self._add_abstract_body(txt) # 英文摘要正文
|
||||
continue
|
||||
# Normal body
|
||||
self._ensure_main_section(state)
|
||||
state = "main"
|
||||
if txt.startswith("关键词:"):
|
||||
self._add_keywords(txt, cn=True)
|
||||
elif txt.startswith("Key words:"):
|
||||
self._add_keywords(txt, cn=False)
|
||||
else:
|
||||
self._add_body_para(txt)
|
||||
continue
|
||||
|
||||
if t == "block_code":
|
||||
# code can appear in abstract or main — skip abstract code
|
||||
if state in ("abstract_cn", "abstract_en", "toc"):
|
||||
continue
|
||||
self._ensure_main_section(state)
|
||||
state = "main"
|
||||
self._process_code(blk)
|
||||
continue
|
||||
|
||||
if t == "block_quote":
|
||||
txt = blk.get("text", "").strip()
|
||||
if not txt:
|
||||
continue
|
||||
self._ensure_main_section(state)
|
||||
state = "main"
|
||||
self._add_body_para(txt)
|
||||
continue
|
||||
|
||||
if t == "list":
|
||||
self._ensure_main_section(state)
|
||||
state = "main"
|
||||
for item in blk.get("items", []):
|
||||
self._add_body_para("• " + item)
|
||||
continue
|
||||
|
||||
if t == "thematic_break":
|
||||
# In front matter or already processed — handled by state
|
||||
continue
|
||||
|
||||
def _ensure_main_section(self, state: str):
|
||||
if state in ("before_abstract", "abstract_cn", "abstract_en", "toc"):
|
||||
if not self._section_break_added:
|
||||
self._add_section_break_main()
|
||||
|
||||
def _add_page_break_if_not_first(self):
|
||||
if self._seen_first_chapter:
|
||||
self.doc.add_page_break()
|
||||
else:
|
||||
self._seen_first_chapter = True
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# rendering methods
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
|
||||
# ── abstract ──────────────────────────────────────────────────
|
||||
|
||||
def _add_abstract_title(self, text: str):
|
||||
"""摘要题头:三号宋体加粗居中 (3.3节)"""
|
||||
cfg = self.config
|
||||
p = self.doc.add_paragraph()
|
||||
p.style = self.doc.styles["Heading 1"]
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
_set_spacing(p, before=0, after=0,
|
||||
line_spacing=cfg.line_spacing_heading)
|
||||
run = p.add_run(text)
|
||||
_set_font(run, cfg.font_cn_heading, cfg.font_heading_en,
|
||||
size=cfg.size_abstract_title, bold=True)
|
||||
# blank line after title (§3.3)
|
||||
self.doc.add_paragraph()
|
||||
|
||||
def _add_abstract_body(self, text: str):
|
||||
"""摘要正文:小四宋体,首行缩进2字符"""
|
||||
cfg = self.config
|
||||
p = self.doc.add_paragraph()
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
|
||||
_set_spacing(p, before=0, after=0,
|
||||
line_spacing=cfg.line_spacing_body)
|
||||
_set_indent(p, cfg.first_line_indent_chars)
|
||||
tokens = _parse_inline(text)
|
||||
_add_inline(p, tokens, cfg)
|
||||
|
||||
def _add_abstract_title_en(self):
|
||||
"""英文摘要页:标题+论文题目+作者署名 (2.3节)"""
|
||||
cfg = self.config
|
||||
p = self.doc.add_paragraph()
|
||||
p.style = self.doc.styles["Heading 1"]
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
_set_spacing(p, before=0, after=0,
|
||||
line_spacing=cfg.line_spacing_heading)
|
||||
run = p.add_run("Abstract")
|
||||
_set_font(run, cfg.font_cn_heading, cfg.font_heading_en,
|
||||
size=cfg.size_abstract_title, bold=True)
|
||||
self.doc.add_paragraph()
|
||||
|
||||
# Thesis title in English (centered)
|
||||
if self._thesis_title:
|
||||
# crude English translation placeholder — user should replace
|
||||
p2 = self.doc.add_paragraph()
|
||||
p2.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
_set_spacing(p2, before=0, after=0,
|
||||
line_spacing=cfg.line_spacing_heading)
|
||||
r = p2.add_run(self._thesis_title)
|
||||
_set_font(r, cfg.font_cn_heading, cfg.font_heading_en,
|
||||
size=cfg.size_section, bold=True)
|
||||
|
||||
# Author & teacher line
|
||||
p3 = self.doc.add_paragraph()
|
||||
p3.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
_set_spacing(p3, before=6, after=6,
|
||||
line_spacing=cfg.line_spacing_heading)
|
||||
r = p3.add_run("Student: \tTeacher: ")
|
||||
_set_font(r, cfg.font_cn, cfg.font_en, size=cfg.size_body)
|
||||
|
||||
# ── keywords ──────────────────────────────────────────────────
|
||||
|
||||
def _add_keywords(self, text: str, cn: bool):
|
||||
"""关键词:小四宋体加粗顶格 (3.3节)"""
|
||||
cfg = self.config
|
||||
p = self.doc.add_paragraph()
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
_set_spacing(p, before=0, after=0,
|
||||
line_spacing=cfg.line_spacing_body)
|
||||
|
||||
label = cfg.keywords_label_cn if cn else cfg.keywords_label_en
|
||||
m = re.match(r"\*\*" + re.escape(label) + r"\*\*(.*)", text)
|
||||
if m:
|
||||
run = p.add_run(label)
|
||||
_set_font(run, cfg.font_cn_heading, cfg.font_heading_en,
|
||||
size=cfg.size_keyword_label, bold=True)
|
||||
rest = m.group(1).strip()
|
||||
tokens = _parse_inline(rest)
|
||||
_add_inline(p, tokens, cfg, size=cfg.size_keyword_label)
|
||||
else:
|
||||
tokens = _parse_inline(text)
|
||||
_add_inline(p, tokens, cfg, size=cfg.size_keyword_label)
|
||||
|
||||
# ── TOC ────────────────────────────────────────────────────────
|
||||
|
||||
def _add_toc(self, title: str):
|
||||
cfg = self.config
|
||||
p = self.doc.add_paragraph()
|
||||
p.style = self.doc.styles["Heading 1"]
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
_set_spacing(p, before=0, after=0,
|
||||
line_spacing=cfg.line_spacing_heading)
|
||||
run = p.add_run(title)
|
||||
_set_font(run, cfg.font_cn_heading, cfg.font_heading_en,
|
||||
size=cfg.size_abstract_title, bold=True)
|
||||
|
||||
self.doc.add_paragraph() # blank line
|
||||
|
||||
# Word TOC field
|
||||
p2 = self.doc.add_paragraph()
|
||||
_set_spacing(p2, before=0, after=0,
|
||||
line_spacing=cfg.line_spacing_body)
|
||||
r = p2.add_run()
|
||||
r._element.append(parse_xml(
|
||||
f'<w:fldChar {nsdecls("w")} w:fldCharType="begin"/>'))
|
||||
r2 = p2.add_run()
|
||||
r2._element.append(parse_xml(
|
||||
f'<w:instrText {nsdecls("w")} xml:space="preserve">'
|
||||
' TOC \\o "1-3" \\h \\z \\u </w:instrText>'))
|
||||
r3 = p2.add_run()
|
||||
r3._element.append(parse_xml(
|
||||
f'<w:fldChar {nsdecls("w")} w:fldCharType="separate"/>'))
|
||||
r4 = p2.add_run("(请右键此处 > 更新域)")
|
||||
_set_font(r4, cfg.font_cn, cfg.font_en, size=cfg.size_body)
|
||||
r5 = p2.add_run()
|
||||
r5._element.append(parse_xml(
|
||||
f'<w:fldChar {nsdecls("w")} w:fldCharType="end"/>'))
|
||||
|
||||
# ── chapter headings (第一层次) ──────────────────────────────
|
||||
|
||||
def _add_chapter(self, text: str):
|
||||
"""章标题:三号宋体加粗,顶格 (§3.2 表3)"""
|
||||
cfg = self.config
|
||||
p = self.doc.add_paragraph()
|
||||
p.style = self.doc.styles["Heading 1"]
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
_set_spacing(p, before=0, after=0,
|
||||
line_spacing=cfg.line_spacing_heading)
|
||||
|
||||
# Ensure double space between number and title (§2.5.2 表1)
|
||||
formatted = re.sub(r"^(\d+)\s+", r"\1 ", text)
|
||||
run = p.add_run(formatted)
|
||||
_set_font(run, cfg.font_cn_heading, cfg.font_heading_en,
|
||||
size=cfg.size_chapter, bold=True)
|
||||
|
||||
# ── section heading (第二层次) ───────────────────────────────
|
||||
|
||||
def _add_section(self, text: str):
|
||||
"""节标题:小三号宋体加粗,顶格 (§3.2 表3)"""
|
||||
cfg = self.config
|
||||
p = self.doc.add_paragraph()
|
||||
p.style = self.doc.styles["Heading 2"]
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
_set_spacing(p, before=0, after=0,
|
||||
line_spacing=cfg.line_spacing_heading)
|
||||
|
||||
# Single space between number and title (§2.5.2 表1)
|
||||
formatted = re.sub(r"^(\d+\.\d+)\s+", r"\1 ", text)
|
||||
run = p.add_run(formatted)
|
||||
_set_font(run, cfg.font_cn_heading, cfg.font_heading_en,
|
||||
size=cfg.size_section, bold=True)
|
||||
|
||||
# ── subsection heading (第三层次) ──────────────────────────
|
||||
|
||||
def _add_subsection(self, text: str):
|
||||
"""条标题:四号宋体加粗,顶格 (§3.2 表3)"""
|
||||
cfg = self.config
|
||||
p = self.doc.add_paragraph()
|
||||
p.style = self.doc.styles["Heading 3"]
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
_set_spacing(p, before=0, after=0,
|
||||
line_spacing=cfg.line_spacing_heading)
|
||||
run = p.add_run(text)
|
||||
_set_font(run, cfg.font_cn_heading, cfg.font_heading_en,
|
||||
size=cfg.size_subsection, bold=True)
|
||||
|
||||
# ── body paragraph ──────────────────────────────────────────
|
||||
|
||||
def _add_body_para(self, text: str, bold: bool = False,
|
||||
indent: bool = True):
|
||||
"""正文:小四宋体,首行缩进2字符 (§3.2)"""
|
||||
cfg = self.config
|
||||
p = self.doc.add_paragraph()
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
|
||||
_set_spacing(p, before=0, after=0,
|
||||
line_spacing=cfg.line_spacing_body)
|
||||
if indent:
|
||||
_set_indent(p, cfg.first_line_indent_chars)
|
||||
tokens = _parse_inline(text)
|
||||
_add_inline(p, tokens, cfg, bold=bold)
|
||||
|
||||
# ── code block ──────────────────────────────────────────────
|
||||
|
||||
def _process_code(self, blk: dict):
|
||||
code = blk.get("raw", "")
|
||||
if not code.strip():
|
||||
return
|
||||
cfg = self.config
|
||||
p = self.doc.add_paragraph()
|
||||
_set_spacing(p, before=0, after=0,
|
||||
line_spacing=cfg.line_spacing_code)
|
||||
pf = p.paragraph_format
|
||||
pf.left_indent = Cm(0.75)
|
||||
|
||||
pPr = p._element.get_or_add_pPr()
|
||||
shd = parse_xml(
|
||||
f'<w:shd {nsdecls("w")} w:fill="F2F2F2" w:val="clear"/>')
|
||||
pPr.append(shd)
|
||||
|
||||
for line in code.split("\n"):
|
||||
if line:
|
||||
run = p.add_run(line)
|
||||
_set_font(run, cfg.font_code, cfg.font_code,
|
||||
size=cfg.size_code)
|
||||
p.add_run("\n")
|
||||
11
main.py
11
main.py
@@ -1,10 +1,7 @@
|
||||
"""CLI entry for thesis Markdown → Word conversion.
|
||||
#!/usr/bin/env python3
|
||||
"""快速入口(兼容旧用法):python main.py"""
|
||||
|
||||
Usage:
|
||||
uv run python main.py 毕业论文初稿.md # → 毕业论文初稿.docx
|
||||
uv run python main.py 毕业论文初稿.md 论文.docx
|
||||
"""
|
||||
|
||||
from docx_thesis.cli import main
|
||||
from transit.__main__ import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -5,15 +5,20 @@ description = "毕业论文 Markdown → Word 格式转换工具"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.14"
|
||||
dependencies = [
|
||||
"docxtpl>=0.20.2",
|
||||
"mistune>=3.2.1",
|
||||
"python-docx>=1.2.0",
|
||||
"pyyaml>=6.0.3",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
thesis = "docx_thesis.cli:main"
|
||||
thesis = "transit.__main__:main"
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"black>=26.3.1",
|
||||
"mypy>=2.0.0",
|
||||
"olefile>=0.47",
|
||||
"pywin32>=311",
|
||||
"ruff>=0.15.12",
|
||||
]
|
||||
|
||||
15
transit/__init__.py
Normal file
15
transit/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""transit —— 毕业论文 Markdown → Word 格式转换工具。"""
|
||||
|
||||
from .parser import parse_markdown
|
||||
from .body import body_to_paragraphs, replace_placeholder
|
||||
from .renderer import generate_thesis
|
||||
from .config import load_config, ThesisConfig
|
||||
|
||||
__all__ = [
|
||||
"parse_markdown",
|
||||
"body_to_paragraphs",
|
||||
"replace_placeholder",
|
||||
"generate_thesis",
|
||||
"load_config",
|
||||
"ThesisConfig",
|
||||
]
|
||||
34
transit/__main__.py
Normal file
34
transit/__main__.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""CLI 入口:python -m transit"""
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from .renderer import generate_thesis
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="毕业论文 Markdown → Word 格式转换工具"
|
||||
)
|
||||
parser.add_argument("data", type=str, help="Markdown 正文文件路径(.md)")
|
||||
parser.add_argument(
|
||||
"-t", "--template", default="sample.docx", help="docx 模板文件路径(默认: sample.docx)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o", "--output", default="output.docx", help="输出 Word 文件路径(默认: output.docx)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c", "--config", default=None, help="TOML 配置文件路径(可选)"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
generate_thesis(
|
||||
template_path=args.template,
|
||||
data_path=args.data,
|
||||
config_path=args.config,
|
||||
output_path=args.output,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
87
transit/body.py
Normal file
87
transit/body.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""
|
||||
Markdown 正文 → Word 段落转换。
|
||||
|
||||
将正文 Markdown 按标题层级拆分为带样式的段落序列,
|
||||
再注入到渲染后 docx 文档的占位符位置。
|
||||
"""
|
||||
|
||||
import re
|
||||
from docx import Document
|
||||
|
||||
_PAT_HEADING = re.compile(r"^(#{1,6})\s+(.+)$", re.MULTILINE)
|
||||
|
||||
|
||||
def body_to_paragraphs(md_text: str) -> list[dict]:
|
||||
"""将 Markdown 正文按标题和段落拆分为结构化列表。
|
||||
|
||||
返回的每个元素::
|
||||
{"text": str, "level": int, "style": str}
|
||||
其中 ``style`` 为 ``Heading N`` 或 ``Normal``。
|
||||
"""
|
||||
paragraphs: list[dict] = []
|
||||
last_end = 0
|
||||
|
||||
for m in _PAT_HEADING.finditer(md_text):
|
||||
# 标题前的普通文本
|
||||
if m.start() > last_end:
|
||||
pre = md_text[last_end : m.start()].strip()
|
||||
if pre:
|
||||
for block in re.split(r"\n\s*\n", pre):
|
||||
block = block.strip()
|
||||
if block:
|
||||
paragraphs.append(
|
||||
{"text": block, "level": 0, "style": "Normal"}
|
||||
)
|
||||
|
||||
level = len(m.group(1))
|
||||
heading_text = m.group(2).strip()
|
||||
paragraphs.append(
|
||||
{"text": heading_text, "level": level, "style": f"Heading {level}"}
|
||||
)
|
||||
last_end = m.end()
|
||||
|
||||
# 最后一段 / 尾部文本
|
||||
tail = md_text[last_end:].strip()
|
||||
if tail:
|
||||
for block in re.split(r"\n\s*\n", tail):
|
||||
block = block.strip()
|
||||
if block:
|
||||
paragraphs.append(
|
||||
{"text": block, "level": 0, "style": "Normal"}
|
||||
)
|
||||
|
||||
return paragraphs
|
||||
|
||||
|
||||
def replace_placeholder(doc: Document, placeholder: str, paragraphs: list[dict]):
|
||||
"""在 *doc* 中找到包含 *placeholder* 的段落,替换为 *paragraphs* 列表。
|
||||
|
||||
每个段落的 ``style`` 字段会从文档样式中查找并应用。
|
||||
"""
|
||||
placeholder_found = False
|
||||
for para in doc.paragraphs:
|
||||
if placeholder in para.text:
|
||||
placeholder_found = True
|
||||
parent = para._element.getparent()
|
||||
idx = list(parent).index(para._element)
|
||||
parent.remove(para._element)
|
||||
|
||||
for pd_data in reversed(paragraphs):
|
||||
new_p = doc.add_paragraph(pd_data["text"])
|
||||
style_name = pd_data["style"]
|
||||
try:
|
||||
new_p.style = doc.styles[style_name]
|
||||
except KeyError:
|
||||
matched = False
|
||||
for s in doc.styles:
|
||||
if s.name.lower() == style_name.lower():
|
||||
new_p.style = s
|
||||
matched = True
|
||||
break
|
||||
if not matched:
|
||||
new_p.style = doc.styles["Normal"]
|
||||
parent.insert(idx, new_p._element)
|
||||
break
|
||||
|
||||
if not placeholder_found:
|
||||
print(f"警告:未找到占位符 '{placeholder}',正文段落未注入。")
|
||||
63
transit/config.py
Normal file
63
transit/config.py
Normal file
@@ -0,0 +1,63 @@
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
import tomllib
|
||||
|
||||
|
||||
@dataclass
|
||||
class ThesisConfig:
|
||||
"""论文配置数据(学生信息、元数据等,不包含正文内容)。"""
|
||||
|
||||
student_name: str = "<None>"
|
||||
student_id: str = "<None>"
|
||||
college: str = "<None>"
|
||||
major: str = "<None>"
|
||||
class_: str = "<None>"
|
||||
advisor: str = "<None>"
|
||||
advisor_title: str = "<None>"
|
||||
title: str = "<None>"
|
||||
|
||||
title_from_md: bool = True
|
||||
body_start_keywords: list[str] = field(default_factory=lambda: ["绪论", "引言"])
|
||||
body_end_keywords: list[str] = field(
|
||||
default_factory=lambda: ["致谢", "参考文献", "附录"]
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""转成模板渲染用的扁平字典,排除 options 命名空间。"""
|
||||
return {
|
||||
"student_name": self.student_name,
|
||||
"student_id": self.student_id,
|
||||
"college": self.college,
|
||||
"major": self.major,
|
||||
"class": self.class_,
|
||||
"advisor": self.advisor,
|
||||
"advisor_title": self.advisor_title,
|
||||
"title": self.title,
|
||||
}
|
||||
|
||||
|
||||
def load_config(path: str | Path) -> ThesisConfig:
|
||||
"""从 TOML 文件加载论文配置。"""
|
||||
path = Path(path)
|
||||
with open(path, "rb") as f:
|
||||
raw = tomllib.load(f)
|
||||
|
||||
meta = raw.get("metadata", {})
|
||||
opts = raw.get("options", {})
|
||||
|
||||
return ThesisConfig(
|
||||
student_name=meta.get("student_name", "<None>"),
|
||||
student_id=meta.get("student_id", "<None>"),
|
||||
college=meta.get("college", "<None>"),
|
||||
major=meta.get("major", "<None>"),
|
||||
class_=meta.get("class", "<None>"),
|
||||
advisor=meta.get("advisor", "<None>"),
|
||||
advisor_title=meta.get("advisor_title", "<None>"),
|
||||
title=meta.get("title", "<None>"),
|
||||
title_from_md=opts.get("title_from_md", True),
|
||||
body_start_keywords=opts.get("body_start_keywords", ["绪论", "引言"]),
|
||||
body_end_keywords=opts.get(
|
||||
"body_end_keywords", ["致谢", "参考文献", "附录"]
|
||||
),
|
||||
)
|
||||
131
transit/parser.py
Normal file
131
transit/parser.py
Normal file
@@ -0,0 +1,131 @@
|
||||
"""
|
||||
Markdown 论文解析器。
|
||||
|
||||
将结构化 Markdown(摘要、正文、致谢、参考文献、附录)解析为字典,
|
||||
供 docx 模板渲染使用。
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
# 匹配任意级数的标题:^## 或 ^### 等,支持可选的数字编号
|
||||
_RE_HEADING = re.compile(r"^(#{1,6})\s*(?:\d+(?:\.\d+)*\s*)?(.+)$", re.MULTILINE)
|
||||
|
||||
|
||||
def _strip_front_matter(content: str) -> str:
|
||||
"""移除 YAML front matter(``---`` 包裹的头部块)。"""
|
||||
if content.startswith("---"):
|
||||
end = content.find("---", 3)
|
||||
if end != -1:
|
||||
return content[end + 3 :]
|
||||
return content
|
||||
|
||||
|
||||
def _find_section(
|
||||
content: str, titles: list[str], after: int = 0
|
||||
) -> Optional[tuple[int, int, str]]:
|
||||
"""查找第一个匹配的章节,返回 ``(section_start, section_end, matched_title)``。
|
||||
|
||||
章节范围从标题行开始,到下一个同级/更高级标题结束(或内容结尾)。
|
||||
"""
|
||||
for m in _RE_HEADING.finditer(content, after):
|
||||
raw_text = m.group(2).strip()
|
||||
for t in titles:
|
||||
if raw_text == t or raw_text.endswith(t):
|
||||
rest = content[m.end() :]
|
||||
next_m = _RE_HEADING.search(rest)
|
||||
section_end = m.end() + (next_m.start() if next_m else len(rest))
|
||||
return (m.start(), section_end, t)
|
||||
return None
|
||||
|
||||
|
||||
def _get_section_body(content: str, section_info: tuple) -> str:
|
||||
"""从 section_info 中提取标题行之后的纯章节正文。"""
|
||||
hdr_m = _RE_HEADING.match(content, section_info[0])
|
||||
if not hdr_m:
|
||||
return ""
|
||||
return content[hdr_m.end() : section_info[1]].strip()
|
||||
|
||||
|
||||
def parse_markdown(
|
||||
md_text: str,
|
||||
body_start_kw: list[str] | None = None,
|
||||
body_end_kw: list[str] | None = None,
|
||||
) -> dict:
|
||||
"""解析 Markdown 格式的论文文本,返回模板变量字典。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
md_text : str
|
||||
完整的 Markdown 文本。
|
||||
body_start_kw : list[str] | None
|
||||
标识正文开始的章节名列表,默认 [``绪论``, ``引言``]。
|
||||
body_end_kw : list[str] | None
|
||||
标识正文结束的章节名列表,默认 [``致谢``, ``参考文献``, ``附录``]。
|
||||
"""
|
||||
if body_start_kw is None:
|
||||
body_start_kw = ["绪论", "引言"]
|
||||
if body_end_kw is None:
|
||||
body_end_kw = ["致谢", "参考文献", "附录"]
|
||||
|
||||
content = _strip_front_matter(md_text.strip())
|
||||
data: dict = {}
|
||||
|
||||
# ── 标题(第一个 # 标题) ──
|
||||
title_m = re.search(r"^#\s+(.+)$", content, re.MULTILINE)
|
||||
if title_m:
|
||||
data["title"] = title_m.group(1).strip()
|
||||
|
||||
# ── 中文摘要 ──
|
||||
abs_cn = _find_section(content, ["摘 要", "摘要"])
|
||||
if abs_cn:
|
||||
sec_body = _get_section_body(content, abs_cn)
|
||||
kw_m = re.search(
|
||||
r"\*\*关键词[::]?\s*\*\*\s*(.*?)$", sec_body, re.MULTILINE
|
||||
)
|
||||
if kw_m:
|
||||
data["abstact_cn_context"] = sec_body[: kw_m.start()].strip()
|
||||
data["abstract_cn_keywords"] = kw_m.group(1).strip()
|
||||
else:
|
||||
data["abstact_cn_context"] = sec_body
|
||||
|
||||
# ── 英文摘要 ──
|
||||
abs_en = _find_section(content, ["Abstract"])
|
||||
if abs_en:
|
||||
sec_body = _get_section_body(content, abs_en)
|
||||
kw_m = re.search(
|
||||
r"\*\*Key words[::]?\s*\*\*\s*(.*?)$", sec_body, re.MULTILINE
|
||||
)
|
||||
if kw_m:
|
||||
data["abstract_en_context"] = sec_body[: kw_m.start()].strip()
|
||||
data["abstract_en_keywords"] = kw_m.group(1).strip()
|
||||
else:
|
||||
data["abstract_en_context"] = sec_body
|
||||
|
||||
# ── 正文(从绪论/引言到致谢/参考文献/附录) ──
|
||||
body_start = _find_section(content, body_start_kw)
|
||||
if body_start:
|
||||
body_content = content[body_start[0] :]
|
||||
body_end = _find_section(body_content, body_end_kw)
|
||||
if body_end:
|
||||
body_content = body_content[: body_end[0]]
|
||||
data["body_md"] = body_content.strip()
|
||||
else:
|
||||
data["body_md"] = ""
|
||||
|
||||
# ── 致谢 ──
|
||||
ack = _find_section(content, ["致谢"])
|
||||
if ack:
|
||||
data["acknowledgement"] = content[ack[0] : ack[1]].strip()
|
||||
|
||||
# ── 参考文献 ──
|
||||
ref = _find_section(content, ["参考文献"])
|
||||
if ref:
|
||||
data["reference"] = content[ref[0] : ref[1]].strip()
|
||||
|
||||
# ── 附录 ──
|
||||
app = _find_section(content, ["附录"])
|
||||
if app:
|
||||
data["appendix"] = content[app[0] : app[1]].strip()
|
||||
|
||||
return data
|
||||
132
transit/renderer.py
Normal file
132
transit/renderer.py
Normal file
@@ -0,0 +1,132 @@
|
||||
"""
|
||||
论文生成编排器。
|
||||
|
||||
组装 配置 + 解析 + 模板渲染 + 正文注入 的完整流水线。
|
||||
"""
|
||||
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from docxtpl import DocxTemplate
|
||||
from docx import Document
|
||||
|
||||
from .config import load_config, ThesisConfig
|
||||
from .parser import parse_markdown
|
||||
from .body import body_to_paragraphs, replace_placeholder
|
||||
|
||||
|
||||
_TEXT_FIELDS = [
|
||||
"title",
|
||||
"abstact_cn_context",
|
||||
"abstract_cn_keywords",
|
||||
"abstract_en_context",
|
||||
"abstract_en_keywords",
|
||||
"acknowledgement",
|
||||
"reference",
|
||||
"appendix",
|
||||
"student_name",
|
||||
"student_id",
|
||||
"college",
|
||||
"major",
|
||||
"class",
|
||||
"advisor",
|
||||
"advisor_title",
|
||||
]
|
||||
|
||||
|
||||
def generate_thesis(
|
||||
template_path: str | Path,
|
||||
data_path: str | Path,
|
||||
config_path: str | Path | None = None,
|
||||
output_path: str | Path = "output.docx",
|
||||
) -> dict:
|
||||
"""执行从数据到 Word 的完整论文生成流程。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
template_path : str | Path
|
||||
docxtpl 模板文件路径(.docx)。
|
||||
data_path : str | Path
|
||||
Markdown 论文正文文件路径(.md)。
|
||||
config_path : str | Path | None
|
||||
TOML 配置文件路径。为 ``None`` 时尝试自动查找。
|
||||
output_path : str | Path
|
||||
输出 Word 文件路径。
|
||||
"""
|
||||
data_path = Path(data_path)
|
||||
|
||||
# 1. 加载配置
|
||||
if config_path is None:
|
||||
candidates = [
|
||||
Path("thesis_config.toml"),
|
||||
data_path.with_suffix(".toml"),
|
||||
]
|
||||
config_path = next((p for p in candidates if p.exists()), None)
|
||||
|
||||
config: ThesisConfig | None = None
|
||||
if config_path and Path(config_path).exists():
|
||||
config = load_config(config_path)
|
||||
print(f"[配置] 配置文件: {config_path}")
|
||||
else:
|
||||
config = ThesisConfig()
|
||||
print("[配置] 未找到配置文件,使用默认值。")
|
||||
|
||||
# 2. 解析 Markdown
|
||||
with open(data_path, "r", encoding="utf-8") as f:
|
||||
md_text = f.read()
|
||||
|
||||
context = parse_markdown(
|
||||
md_text,
|
||||
body_start_kw=config.body_start_keywords,
|
||||
body_end_kw=config.body_end_keywords,
|
||||
)
|
||||
|
||||
# 3. 合并配置 → 上下文(配置优先)
|
||||
for k, v in config.to_dict().items():
|
||||
if k == "title" and config.title_from_md and context.get("title"):
|
||||
continue # 以 markdown 标题为准
|
||||
if v != "<None>":
|
||||
context[k] = v
|
||||
|
||||
# 4. 用 defaultdict 兜底缺失键
|
||||
ctx = defaultdict(lambda: "<None>", context)
|
||||
|
||||
# 5. 解析正文为段落列表
|
||||
body_md = ctx.get("body_md", "")
|
||||
body_paragraphs = body_to_paragraphs(body_md) if body_md else []
|
||||
|
||||
# 6. 占位符
|
||||
ctx["body_placeholder"] = "__CONTEXT_PLACEHOLDER__"
|
||||
|
||||
# 7. 渲染模板
|
||||
doc = DocxTemplate(str(template_path))
|
||||
doc.render(ctx)
|
||||
|
||||
# 8. 保存临时文件,再做后处理
|
||||
temp_path = Path(output_path).with_suffix(".tmp")
|
||||
doc.save(str(temp_path))
|
||||
|
||||
# 9. 正文注入
|
||||
final_doc = Document(str(temp_path))
|
||||
replace_placeholder(final_doc, "__CONTEXT_PLACEHOLDER__", body_paragraphs)
|
||||
final_doc.save(str(output_path))
|
||||
temp_path.unlink(missing_ok=True)
|
||||
|
||||
print(f"[完成] 论文生成完成: {output_path}")
|
||||
|
||||
# 10. 字段填充报告
|
||||
print("\n--- 字段填充情况 ---")
|
||||
for key in _TEXT_FIELDS:
|
||||
val = ctx[key]
|
||||
if val == "<None>":
|
||||
print(f" [缺失] {key}")
|
||||
else:
|
||||
preview = str(val)[:60].replace("\n", " ")
|
||||
print(f" [OK] {key}: {preview}...")
|
||||
|
||||
missing = [k for k in _TEXT_FIELDS if ctx[k] == "<None>"]
|
||||
if missing:
|
||||
print("\n[警告] 以下字段缺失,已填充 '<None>':")
|
||||
for f in missing:
|
||||
print(f" - {f}")
|
||||
|
||||
return dict(ctx)
|
||||
191
uv.lock
generated
191
uv.lock
generated
@@ -44,6 +44,75 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/46/d3ec57ad500f598d1554bd14ce4df615960549ab2844961bc4e1f5fbd174/ast_serialize-0.3.0-cp39-abi3-win_arm64.whl", hash = "sha256:0dd00da29985f15f50dc35728b7e1e7c84507bccfea1d9914738530f1c72238a", size = 1077165, upload-time = "2026-04-30T23:24:46.377Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "26.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "mypy-extensions" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pathspec" },
|
||||
{ name = "platformdirs" },
|
||||
{ name = "pytokens" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e1/c5/61175d618685d42b005847464b8fb4743a67b1b8fdb75e50e5a96c31a27a/black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07", size = 666155, upload-time = "2026-03-12T03:36:03.593Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/da/e36e27c9cebc1311b7579210df6f1c86e50f2d7143ae4fcf8a5017dc8809/black-26.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2d6bfaf7fd0993b420bed691f20f9492d53ce9a2bcccea4b797d34e947318a78", size = 1889234, upload-time = "2026-03-12T03:40:30.964Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/7b/9871acf393f64a5fa33668c19350ca87177b181f44bb3d0c33b2d534f22c/black-26.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f89f2ab047c76a9c03f78d0d66ca519e389519902fa27e7a91117ef7611c0568", size = 1720522, upload-time = "2026-03-12T03:40:32.346Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/87/e766c7f2e90c07fb7586cc787c9ae6462b1eedab390191f2b7fc7f6170a9/black-26.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b07fc0dab849d24a80a29cfab8d8a19187d1c4685d8a5e6385a5ce323c1f015f", size = 1787824, upload-time = "2026-03-12T03:40:33.636Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/94/2424338fb2d1875e9e83eed4c8e9c67f6905ec25afd826a911aea2b02535/black-26.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:0126ae5b7c09957da2bdbd91a9ba1207453feada9e9fe51992848658c6c8e01c", size = 1445855, upload-time = "2026-03-12T03:40:35.442Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/43/0c3338bd928afb8ee7471f1a4eec3bdbe2245ccb4a646092a222e8669840/black-26.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:92c0ec1f2cc149551a2b7b47efc32c866406b6891b0ee4625e95967c8f4acfb1", size = 1258109, upload-time = "2026-03-12T03:40:36.832Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/0d/52d98722666d6fc6c3dd4c76df339501d6efd40e0ff95e6186a7b7f0befd/black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b", size = 207542, upload-time = "2026-03-12T03:36:01.668Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "docxtpl"
|
||||
version = "0.20.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "jinja2" },
|
||||
{ name = "lxml" },
|
||||
{ name = "python-docx" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b2/b4/4435f3fcb1357ec441079c4af1dda3ea926fad6dcead4aed2d93b369944e/docxtpl-0.20.2.tar.gz", hash = "sha256:eddf3350d70b4d123208e801d585bcb313d21044a377a14f75a66d0965841de1", size = 17890, upload-time = "2025-11-13T12:47:15.943Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/ad/e07939d8e020e513d3860400413ba1e0e06102c469639b440d921337efef/docxtpl-0.20.2-py3-none-any.whl", hash = "sha256:626d5c570a46a62b2ca73b4d08f1c240fa031a5bc45371e1466a4fe184923d10", size = 17881, upload-time = "2025-11-13T12:47:13.704Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "librt"
|
||||
version = "0.10.0"
|
||||
@@ -122,6 +191,36 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/a0/9b916c68c0e57752c07f8f64b30138d9d4059dbeb27b90274dedbea128ff/lxml-6.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:26dd9f57ee3bd41e7d35b4c98a2ffd89ed11591649f421f0ec19f67d50ec67ac", size = 3817120, upload-time = "2026-04-18T04:32:15.803Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "3.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mistune"
|
||||
version = "3.2.1"
|
||||
@@ -170,6 +269,24 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "olefile"
|
||||
version = "0.47"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/69/1b/077b508e3e500e1629d366249c3ccb32f95e50258b231705c09e3c7a4366/olefile-0.47.zip", hash = "sha256:599383381a0bf3dfbd932ca0ca6515acd174ed48870cbf7fee123d698c192c1c", size = 112240, upload-time = "2023-12-01T16:22:53.025Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/17/d3/b64c356a907242d719fc668b71befd73324e47ab46c8ebbbede252c154b2/olefile-0.47-py2.py3-none-any.whl", hash = "sha256:543c7da2a7adadf21214938bb79c83ea12b473a4b6ee4ad4bf854e7715e13d1f", size = 114565, upload-time = "2023-12-01T16:22:51.518Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "26.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pathspec"
|
||||
version = "1.1.1"
|
||||
@@ -179,6 +296,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.9.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-docx"
|
||||
version = "1.2.0"
|
||||
@@ -192,6 +318,61 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/00/1e03a4989fa5795da308cd774f05b704ace555a70f9bf9d3be057b680bcf/python_docx-1.2.0-py3-none-any.whl", hash = "sha256:3fd478f3250fbbbfd3b94fe1e985955737c145627498896a8a6bf81f4baf66c7", size = 252987, upload-time = "2025-06-16T20:46:22.506Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytokens"
|
||||
version = "0.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pywin32"
|
||||
version = "311"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "6.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.12"
|
||||
@@ -222,25 +403,35 @@ name = "transit"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "docxtpl" },
|
||||
{ name = "mistune" },
|
||||
{ name = "python-docx" },
|
||||
{ name = "pyyaml" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "black" },
|
||||
{ name = "mypy" },
|
||||
{ name = "olefile" },
|
||||
{ name = "pywin32" },
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "docxtpl", specifier = ">=0.20.2" },
|
||||
{ name = "mistune", specifier = ">=3.2.1" },
|
||||
{ name = "python-docx", specifier = ">=1.2.0" },
|
||||
{ name = "pyyaml", specifier = ">=6.0.3" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "black", specifier = ">=26.3.1" },
|
||||
{ name = "mypy", specifier = ">=2.0.0" },
|
||||
{ name = "olefile", specifier = ">=0.47" },
|
||||
{ name = "pywin32", specifier = ">=311" },
|
||||
{ name = "ruff", specifier = ">=0.15.12" },
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user