221 lines
7.3 KiB
Python
221 lines
7.3 KiB
Python
import os
|
|
import re
|
|
import sys
|
|
import shutil
|
|
from pathlib import Path
|
|
from docx import Document
|
|
from docx.oxml.text.paragraph import CT_P
|
|
from docx.oxml.table import CT_Tbl
|
|
from docx.text.paragraph import Paragraph
|
|
from docx.table import Table
|
|
|
|
def clean_cell_text(cell):
|
|
"""Clean and format table cell text into markdown-compatible single line."""
|
|
# Process cell paragraphs
|
|
lines = []
|
|
for p in cell.paragraphs:
|
|
p_text = format_paragraph(p).strip()
|
|
if p_text:
|
|
lines.append(p_text)
|
|
full_text = "<br>".join(lines)
|
|
# Escape pipes
|
|
full_text = full_text.replace("|", r"\|")
|
|
# Clean excessive whitespace
|
|
full_text = re.sub(r"[ \t]+", " ", full_text).strip()
|
|
return full_text
|
|
|
|
def format_paragraph(p):
|
|
"""Format paragraph runs into markdown text with bold/italic."""
|
|
runs_text = []
|
|
for r in p.runs:
|
|
text = r.text
|
|
if not text:
|
|
continue
|
|
# Check formatting
|
|
is_bold = bool(r.bold)
|
|
is_italic = bool(r.italic)
|
|
|
|
# Don't format pure whitespace runs
|
|
if text.strip() == "":
|
|
runs_text.append(text)
|
|
continue
|
|
|
|
prefix_space = " " if text.startswith(" ") else ""
|
|
suffix_space = " " if text.endswith(" ") else ""
|
|
core_text = text.strip()
|
|
|
|
if is_bold and is_italic:
|
|
core_text = f"***{core_text}***"
|
|
elif is_bold:
|
|
core_text = f"**{core_text}**"
|
|
elif is_italic:
|
|
core_text = f"*{core_text}*"
|
|
|
|
runs_text.append(prefix_space + core_text + suffix_space)
|
|
|
|
res = "".join(runs_text)
|
|
if not res.strip() and p.text.strip():
|
|
# Fallback if runs were empty or weird
|
|
res = p.text.strip()
|
|
return res
|
|
|
|
def convert_paragraph_to_md(p):
|
|
"""Convert a docx Paragraph to Markdown string."""
|
|
text = format_paragraph(p).strip()
|
|
if not text:
|
|
return ""
|
|
|
|
style_name = p.style.name if p.style and p.style.name else ""
|
|
style_lower = style_name.lower()
|
|
|
|
# Check heading styles
|
|
if "heading 1" in style_lower or style_name in ["제목 1", "Title", "제목"]:
|
|
return f"# {text}\n"
|
|
elif "heading 2" in style_lower or style_name in ["제목 2"]:
|
|
return f"## {text}\n"
|
|
elif "heading 3" in style_lower or style_name in ["제목 3", "Subtitle", "부제목"]:
|
|
return f"### {text}\n"
|
|
elif "heading 4" in style_lower or style_name in ["제목 4"]:
|
|
return f"#### {text}\n"
|
|
elif "heading 5" in style_lower or style_name in ["제목 5"]:
|
|
return f"##### {text}\n"
|
|
elif "heading 6" in style_lower or style_name in ["제목 6"]:
|
|
return f"###### {text}\n"
|
|
|
|
# Check list styles
|
|
if "bullet" in style_lower or "글머리" in style_lower:
|
|
return f"- {text}"
|
|
if "number" in style_lower or "번호" in style_lower:
|
|
return f"1. {text}"
|
|
|
|
return text
|
|
|
|
def convert_table_to_md(table):
|
|
"""Convert a docx Table to Markdown table format."""
|
|
if not table.rows:
|
|
return ""
|
|
|
|
rows_data = []
|
|
max_cols = 0
|
|
for row in table.rows:
|
|
row_cells = []
|
|
for cell in row.cells:
|
|
cell_text = clean_cell_text(cell)
|
|
row_cells.append(cell_text)
|
|
max_cols = max(max_cols, len(row_cells))
|
|
rows_data.append(row_cells)
|
|
|
|
if max_cols == 0:
|
|
return ""
|
|
|
|
# Standardize column count
|
|
norm_rows = []
|
|
for row in rows_data:
|
|
if len(row) < max_cols:
|
|
row.extend([""] * (max_cols - len(row)))
|
|
elif len(row) > max_cols:
|
|
row = row[:max_cols]
|
|
norm_rows.append(row)
|
|
|
|
if not norm_rows:
|
|
return ""
|
|
|
|
md_lines = []
|
|
# If table has only 1 row, format as key-value or 1-row table with header
|
|
if len(norm_rows) == 1:
|
|
# 1-row table: row 0 as header or data row
|
|
header = norm_rows[0]
|
|
md_lines.append("| " + " | ".join(header) + " |")
|
|
md_lines.append("| " + " | ".join(["---"] * max_cols) + " |")
|
|
else:
|
|
# Header row
|
|
header = norm_rows[0]
|
|
md_lines.append("| " + " | ".join(header) + " |")
|
|
md_lines.append("| " + " | ".join(["---"] * max_cols) + " |")
|
|
# Data rows
|
|
for row in norm_rows[1:]:
|
|
md_lines.append("| " + " | ".join(row) + " |")
|
|
|
|
return "\n".join(md_lines) + "\n"
|
|
|
|
def convert_docx_file(docx_path):
|
|
"""Read a docx file and convert its contents to markdown."""
|
|
doc = Document(docx_path)
|
|
md_blocks = []
|
|
|
|
for block in doc.element.body:
|
|
if isinstance(block, CT_P):
|
|
p = Paragraph(block, doc)
|
|
md_text = convert_paragraph_to_md(p)
|
|
if md_text:
|
|
md_blocks.append(md_text)
|
|
elif isinstance(block, CT_Tbl):
|
|
t = Table(block, doc)
|
|
md_table = convert_table_to_md(t)
|
|
if md_table:
|
|
md_blocks.append(md_table)
|
|
|
|
content = "\n\n".join(md_blocks)
|
|
# Normalize consecutive empty lines
|
|
content = re.sub(r"\n{3,}", "\n\n", content).strip() + "\n"
|
|
return content
|
|
|
|
def run_conversion(src_dir="scenario_docx", dst_dir="scenario_md"):
|
|
"""Convert all docx files in src_dir to md files in dst_dir, copying existing md files."""
|
|
src_path = Path(src_dir)
|
|
dst_path = Path(dst_dir)
|
|
|
|
dst_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
converted_count = 0
|
|
copied_count = 0
|
|
error_count = 0
|
|
|
|
for root, dirs, files in os.walk(src_path):
|
|
rel_dir = Path(root).relative_to(src_path)
|
|
target_dir = dst_path / rel_dir
|
|
target_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
for file_name in files:
|
|
src_file = Path(root) / file_name
|
|
ext = src_file.suffix.lower()
|
|
|
|
if ext == ".docx":
|
|
target_file_name = src_file.stem + ".md"
|
|
target_file = target_dir / target_file_name
|
|
try:
|
|
md_content = convert_docx_file(str(src_file))
|
|
with open(target_file, "w", encoding="utf-8") as f:
|
|
f.write(md_content)
|
|
converted_count += 1
|
|
except Exception as e:
|
|
print(f"[ERROR] Failed to convert {src_file}: {e}", file=sys.stderr)
|
|
error_count += 1
|
|
elif ext == ".md":
|
|
target_file = target_dir / file_name
|
|
try:
|
|
shutil.copy2(src_file, target_file)
|
|
copied_count += 1
|
|
except Exception as e:
|
|
print(f"[ERROR] Failed to copy {src_file}: {e}", file=sys.stderr)
|
|
error_count += 1
|
|
else:
|
|
# Other files (e.g. assets, images)
|
|
target_file = target_dir / file_name
|
|
try:
|
|
shutil.copy2(src_file, target_file)
|
|
copied_count += 1
|
|
except Exception as e:
|
|
print(f"[ERROR] Failed to copy {src_file}: {e}", file=sys.stderr)
|
|
error_count += 1
|
|
|
|
print("=" * 60)
|
|
print("Conversion Summary:")
|
|
print(f" Converted .docx -> .md : {converted_count}")
|
|
print(f" Copied .md / others : {copied_count}")
|
|
print(f" Errors : {error_count}")
|
|
print("=" * 60)
|
|
|
|
if __name__ == "__main__":
|
|
run_conversion()
|