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 = "
".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()