161 lines
5.4 KiB
Python
161 lines
5.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Small, original DOCX helper for structural inspection and simple edits."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
from pathlib import Path
|
|
|
|
from docx import Document
|
|
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
|
from docx.shared import Cm, Pt
|
|
|
|
|
|
def ensure_distinct(source: Path, output: Path) -> None:
|
|
if source.resolve() == output.resolve():
|
|
raise SystemExit("Refusing to overwrite the input file; choose a different output path.")
|
|
|
|
|
|
def inspect_docx(path: Path) -> None:
|
|
doc = Document(path)
|
|
headings = []
|
|
nonempty = 0
|
|
for index, paragraph in enumerate(doc.paragraphs, start=1):
|
|
text = paragraph.text.strip()
|
|
if text:
|
|
nonempty += 1
|
|
if text and paragraph.style and paragraph.style.name.lower().startswith("heading"):
|
|
headings.append({"paragraph": index, "style": paragraph.style.name, "text": text[:160]})
|
|
|
|
tables = []
|
|
for index, table in enumerate(doc.tables, start=1):
|
|
tables.append(
|
|
{
|
|
"table": index,
|
|
"rows": len(table.rows),
|
|
"columns": max((len(row.cells) for row in table.rows), default=0),
|
|
}
|
|
)
|
|
|
|
relationships = doc.part.rels.values()
|
|
images = sum(1 for rel in relationships if "image" in rel.reltype)
|
|
report = {
|
|
"file": str(path.resolve()),
|
|
"paragraphs": len(doc.paragraphs),
|
|
"nonempty_paragraphs": nonempty,
|
|
"tables": tables,
|
|
"sections": len(doc.sections),
|
|
"images": images,
|
|
"headings": headings,
|
|
}
|
|
print(json.dumps(report, ensure_ascii=False, indent=2))
|
|
|
|
|
|
def add_content(doc: Document, text: str) -> None:
|
|
for raw in text.splitlines():
|
|
line = raw.rstrip()
|
|
if not line:
|
|
doc.add_paragraph()
|
|
elif line.startswith("### "):
|
|
doc.add_heading(line[4:].strip(), level=3)
|
|
elif line.startswith("## "):
|
|
doc.add_heading(line[3:].strip(), level=2)
|
|
elif line.startswith("# "):
|
|
doc.add_heading(line[2:].strip(), level=1)
|
|
elif line.startswith(("- ", "* ")):
|
|
doc.add_paragraph(line[2:].strip(), style="List Bullet")
|
|
elif len(line) > 3 and line[:3].rstrip(".").isdigit() and line[1:3] in {". ", "、"}:
|
|
doc.add_paragraph(line[3:].strip(), style="List Number")
|
|
else:
|
|
doc.add_paragraph(line)
|
|
|
|
|
|
def create_docx(input_path: Path, output: Path, title: str | None) -> None:
|
|
doc = Document()
|
|
section = doc.sections[0]
|
|
section.top_margin = Cm(2.3)
|
|
section.bottom_margin = Cm(2.3)
|
|
section.left_margin = Cm(2.5)
|
|
section.right_margin = Cm(2.5)
|
|
|
|
normal = doc.styles["Normal"]
|
|
normal.font.name = "Microsoft YaHei"
|
|
normal.font.size = Pt(10.5)
|
|
normal.paragraph_format.space_after = Pt(6)
|
|
normal.paragraph_format.line_spacing = 1.35
|
|
|
|
if title:
|
|
paragraph = doc.add_paragraph()
|
|
paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
run = paragraph.add_run(title)
|
|
run.bold = True
|
|
run.font.name = "Microsoft YaHei"
|
|
run.font.size = Pt(22)
|
|
paragraph.paragraph_format.space_after = Pt(18)
|
|
|
|
add_content(doc, input_path.read_text(encoding="utf-8"))
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
doc.save(output)
|
|
inspect_docx(output)
|
|
|
|
|
|
def replace_text(source: Path, output: Path, old: str, new: str) -> None:
|
|
ensure_distinct(source, output)
|
|
doc = Document(source)
|
|
replacements = 0
|
|
for paragraph in doc.paragraphs:
|
|
if old in paragraph.text:
|
|
original = paragraph.text
|
|
for run in paragraph.runs:
|
|
run.text = ""
|
|
paragraph.add_run(original.replace(old, new))
|
|
replacements += original.count(old)
|
|
for table in doc.tables:
|
|
for row in table.rows:
|
|
for cell in row.cells:
|
|
for paragraph in cell.paragraphs:
|
|
if old in paragraph.text:
|
|
original = paragraph.text
|
|
for run in paragraph.runs:
|
|
run.text = ""
|
|
paragraph.add_run(original.replace(old, new))
|
|
replacements += original.count(old)
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
doc.save(output)
|
|
print(json.dumps({"output": str(output.resolve()), "replacements": replacements}, ensure_ascii=False))
|
|
|
|
|
|
def build_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser()
|
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
|
|
inspect_cmd = sub.add_parser("inspect")
|
|
inspect_cmd.add_argument("input", type=Path)
|
|
|
|
create_cmd = sub.add_parser("create")
|
|
create_cmd.add_argument("--input", required=True, type=Path)
|
|
create_cmd.add_argument("--output", required=True, type=Path)
|
|
create_cmd.add_argument("--title")
|
|
|
|
replace_cmd = sub.add_parser("replace")
|
|
replace_cmd.add_argument("input", type=Path)
|
|
replace_cmd.add_argument("--output", required=True, type=Path)
|
|
replace_cmd.add_argument("--old", required=True)
|
|
replace_cmd.add_argument("--new", required=True)
|
|
return parser
|
|
|
|
|
|
def main() -> None:
|
|
args = build_parser().parse_args()
|
|
if args.command == "inspect":
|
|
inspect_docx(args.input)
|
|
elif args.command == "create":
|
|
create_docx(args.input, args.output, args.title)
|
|
elif args.command == "replace":
|
|
replace_text(args.input, args.output, args.old, args.new)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|