Files
olons_admin/tools/convert_admin_prd_to_markdown.py
T

147 lines
4.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import hashlib
import re
from pathlib import Path
from docx import Document
from docx.oxml.ns import qn
from docx.table import Table
from docx.text.paragraph import Paragraph
ROOT = Path(__file__).resolve().parents[1]
DOC_DIR = ROOT / "docs" / "admin_reference_prd"
SOURCE = DOC_DIR / "后台管理系统需求文档.docx"
TARGET = DOC_DIR / "后台管理系统需求文档.md"
ASSET_DIR = DOC_DIR / "assets"
IMAGE_ALTS = {
"prototype_01_dashboard.png": "后台首页看板脱敏原型",
"prototype_02_member_list.png": "会员列表脱敏原型",
"prototype_03_member_modal.png": "会员编辑弹窗原型",
"prototype_04_withdrawal.png": "提现审核脱敏原型",
"prototype_05_rbac.png": "RBAC 角色权限原型",
"prototype_06_flow.png": "C 端与后台业务闭环原型",
}
def iter_blocks(document: Document):
for child in document.element.body.iterchildren():
if child.tag == qn("w:p"):
yield Paragraph(child, document)
elif child.tag == qn("w:tbl"):
yield Table(child, document)
def media_map(document: Document) -> dict[str, str]:
assets_by_hash = {
hashlib.sha256(path.read_bytes()).hexdigest(): path.name
for path in ASSET_DIR.glob("*.png")
}
mapping: dict[str, str] = {}
for rel_id, rel in document.part.rels.items():
if "image" not in rel.reltype:
continue
digest = hashlib.sha256(rel.target_part.blob).hexdigest()
name = assets_by_hash.get(digest)
if name:
mapping[rel_id] = f"assets/{name}"
return mapping
def paragraph_images(paragraph: Paragraph, images: dict[str, str]) -> list[str]:
result: list[str] = []
for blip in paragraph._p.xpath(".//a:blip"):
rel_id = blip.get(qn("r:embed"))
if rel_id in images:
result.append(images[rel_id])
return result
def escape_cell(value: str) -> str:
return value.strip().replace("|", "\\|").replace("\n", "<br>") or "—"
def table_markdown(table: Table) -> list[str]:
rows = [[escape_cell(cell.text) for cell in row.cells] for row in table.rows]
if not rows:
return []
width = max(len(row) for row in rows)
rows = [row + ["—"] * (width - len(row)) for row in rows]
lines = [
"| " + " | ".join(rows[0]) + " |",
"| " + " | ".join(["---"] * width) + " |",
]
lines.extend("| " + " | ".join(row) + " |" for row in rows[1:])
return lines
def github_slug(title: str) -> str:
slug = title.strip().lower()
slug = re.sub(r"[^\w\-\u4e00-\u9fff ]", "", slug)
return re.sub(r"\s+", "-", slug)
def convert() -> None:
document = Document(SOURCE)
images = media_map(document)
body: list[str] = []
top_level: list[str] = []
image_number = 0
for block in iter_blocks(document):
if isinstance(block, Table):
body.extend(table_markdown(block))
body.append("")
continue
text = block.text.strip()
style = block.style.name if block.style else "Normal"
embedded = paragraph_images(block, images)
if style == "Title" and text:
body.extend([f"# {text}", ""])
elif style == "Heading 1" and text:
top_level.append(text)
body.extend([f"## {text}", ""])
elif style == "Heading 2" and text:
body.extend([f"### {text}", ""])
elif style == "List Bullet" and text:
body.extend([f"- {text}", ""])
elif style == "List Number" and text:
body.extend([f"1. {text}", ""])
elif text:
if text.startswith("结论:"):
body.extend([f"> **{text[:3]}** {text[3:]}", ""])
elif text.startswith("P0:"):
body.extend([f"> **P0 风险:** {text[3:]}", ""])
else:
body.extend([text, ""])
for path in embedded:
image_number += 1
alt = IMAGE_ALTS.get(Path(path).name, f"需求原型图 {image_number}")
body.extend([f"![{alt}]({path})", ""])
# Word 封面之后放置 Markdown 可点击目录,去掉 Word 中无意义的空段落。
first_section = next(i for i, line in enumerate(body) if line.startswith("## 1 "))
toc = ["## 目录", ""]
toc.extend(f"- [{title}](#{github_slug(title)})" for title in top_level)
toc.extend(["", "---", ""])
output = body[:first_section] + toc + body[first_section:]
compact: list[str] = []
for line in output:
if line == "" and compact and compact[-1] == "":
continue
compact.append(line)
rendered = "\n".join(compact).rstrip() + "\n"
rendered = re.sub(r"(^- .+)\n\n(?=- )", r"\1\n", rendered, flags=re.MULTILINE)
rendered = re.sub(r"(^1\. .+)\n\n(?=1\. )", r"\1\n", rendered, flags=re.MULTILINE)
TARGET.write_text(rendered, encoding="utf-8")
if __name__ == "__main__":
convert()