634 lines
22 KiB
Python
634 lines
22 KiB
Python
from __future__ import annotations
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
from docx import Document
|
|
from docx.enum.section import WD_SECTION
|
|
from docx.enum.table import WD_CELL_VERTICAL_ALIGNMENT, WD_TABLE_ALIGNMENT
|
|
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_BREAK, WD_LINE_SPACING
|
|
from docx.oxml import OxmlElement
|
|
from docx.oxml.ns import qn
|
|
from docx.shared import Inches, Pt, RGBColor
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parent
|
|
OUTPUT_DIR = ROOT / "docx-professional"
|
|
|
|
SOURCES = [
|
|
(
|
|
ROOT / "01-Tool-개발환경-가이드.md",
|
|
"개발 환경 가이드",
|
|
"Tool Pod 개발 환경과 내부망 반입 기준",
|
|
("개발 환경", "실행·검증", "내부망 반입"),
|
|
),
|
|
(
|
|
ROOT / "02-Tool-설계-가이드.md",
|
|
"Tool 설계 가이드",
|
|
"명명·구조·Schema·MCI·Glow HTTP 설계 기준",
|
|
("명명·패키지", "Input·Output Schema", "MCI·Glow HTTP"),
|
|
),
|
|
(
|
|
ROOT / "03-Tool-개발가이드-1차.md",
|
|
"Tool 개발 가이드 1차본",
|
|
"신규 Tool 구현·검증·완료 절차",
|
|
("Scaffold", "구현·연계", "테스트·완료"),
|
|
),
|
|
]
|
|
|
|
FONT_BODY = "맑은 고딕"
|
|
FONT_CODE = "Consolas"
|
|
BLUE = "2E74B5"
|
|
DARK_BLUE = "1F4D78"
|
|
NAVY = "0B2545"
|
|
INK = "24364B"
|
|
MUTED = "667085"
|
|
LIGHT_BLUE = "E8EEF5"
|
|
LIGHT_GRAY = "F4F6F9"
|
|
SOFT_BLUE = "F1F6FB"
|
|
TABLE_ALT = "F8FAFC"
|
|
BORDER = "C9D2DE"
|
|
WHITE = "FFFFFF"
|
|
GOLD = "B88928"
|
|
CONTENT_DXA = 9360
|
|
TABLE_INDENT_DXA = 120
|
|
|
|
|
|
def set_run_font(run, name=FONT_BODY, size=None, color=None, bold=None, italic=None):
|
|
run.font.name = name
|
|
run._element.get_or_add_rPr().rFonts.set(qn("w:ascii"), name)
|
|
run._element.get_or_add_rPr().rFonts.set(qn("w:hAnsi"), name)
|
|
run._element.get_or_add_rPr().rFonts.set(qn("w:eastAsia"), name)
|
|
if size is not None:
|
|
run.font.size = Pt(size)
|
|
if color is not None:
|
|
run.font.color.rgb = RGBColor.from_string(color)
|
|
if bold is not None:
|
|
run.bold = bold
|
|
if italic is not None:
|
|
run.italic = italic
|
|
|
|
|
|
def set_cell_shading(cell, fill):
|
|
tc_pr = cell._tc.get_or_add_tcPr()
|
|
shd = tc_pr.find(qn("w:shd"))
|
|
if shd is None:
|
|
shd = OxmlElement("w:shd")
|
|
tc_pr.append(shd)
|
|
shd.set(qn("w:fill"), fill)
|
|
|
|
|
|
def set_cell_margins(cell, top=80, start=120, bottom=80, end=120):
|
|
tc = cell._tc
|
|
tc_pr = tc.get_or_add_tcPr()
|
|
tc_mar = tc_pr.first_child_found_in("w:tcMar")
|
|
if tc_mar is None:
|
|
tc_mar = OxmlElement("w:tcMar")
|
|
tc_pr.append(tc_mar)
|
|
for edge, value in (("top", top), ("start", start), ("bottom", bottom), ("end", end)):
|
|
node = tc_mar.find(qn(f"w:{edge}"))
|
|
if node is None:
|
|
node = OxmlElement(f"w:{edge}")
|
|
tc_mar.append(node)
|
|
node.set(qn("w:w"), str(value))
|
|
node.set(qn("w:type"), "dxa")
|
|
|
|
|
|
def set_table_geometry(table, widths):
|
|
total = sum(widths)
|
|
if total != CONTENT_DXA:
|
|
widths[-1] += CONTENT_DXA - total
|
|
table.alignment = WD_TABLE_ALIGNMENT.LEFT
|
|
table.autofit = False
|
|
tbl_pr = table._tbl.tblPr
|
|
tbl_w = tbl_pr.find(qn("w:tblW"))
|
|
if tbl_w is None:
|
|
tbl_w = OxmlElement("w:tblW")
|
|
tbl_pr.append(tbl_w)
|
|
tbl_w.set(qn("w:w"), str(CONTENT_DXA))
|
|
tbl_w.set(qn("w:type"), "dxa")
|
|
tbl_ind = tbl_pr.find(qn("w:tblInd"))
|
|
if tbl_ind is None:
|
|
tbl_ind = OxmlElement("w:tblInd")
|
|
tbl_pr.append(tbl_ind)
|
|
tbl_ind.set(qn("w:w"), str(TABLE_INDENT_DXA))
|
|
tbl_ind.set(qn("w:type"), "dxa")
|
|
grid = table._tbl.tblGrid
|
|
for child in list(grid):
|
|
grid.remove(child)
|
|
for width in widths:
|
|
col = OxmlElement("w:gridCol")
|
|
col.set(qn("w:w"), str(width))
|
|
grid.append(col)
|
|
for row in table.rows:
|
|
for idx, cell in enumerate(row.cells):
|
|
cell.width = Inches(widths[idx] / 1440)
|
|
tc_w = cell._tc.get_or_add_tcPr().find(qn("w:tcW"))
|
|
if tc_w is None:
|
|
tc_w = OxmlElement("w:tcW")
|
|
cell._tc.get_or_add_tcPr().append(tc_w)
|
|
tc_w.set(qn("w:w"), str(widths[idx]))
|
|
tc_w.set(qn("w:type"), "dxa")
|
|
set_cell_margins(cell)
|
|
cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER
|
|
|
|
|
|
def repeat_table_header(row):
|
|
tr_pr = row._tr.get_or_add_trPr()
|
|
header = OxmlElement("w:tblHeader")
|
|
header.set(qn("w:val"), "true")
|
|
tr_pr.append(header)
|
|
|
|
|
|
def add_page_number(paragraph):
|
|
run = paragraph.add_run()
|
|
begin = OxmlElement("w:fldChar")
|
|
begin.set(qn("w:fldCharType"), "begin")
|
|
instruction = OxmlElement("w:instrText")
|
|
instruction.set(qn("xml:space"), "preserve")
|
|
instruction.text = " PAGE "
|
|
separate = OxmlElement("w:fldChar")
|
|
separate.set(qn("w:fldCharType"), "separate")
|
|
text = OxmlElement("w:t")
|
|
text.text = "1"
|
|
end = OxmlElement("w:fldChar")
|
|
end.set(qn("w:fldCharType"), "end")
|
|
run._r.extend([begin, instruction, separate, text, end])
|
|
set_run_font(run, size=9, color=MUTED)
|
|
|
|
|
|
def add_bottom_border(paragraph, color=BORDER, size="6"):
|
|
p_pr = paragraph._p.get_or_add_pPr()
|
|
p_bdr = p_pr.find(qn("w:pBdr"))
|
|
if p_bdr is None:
|
|
p_bdr = OxmlElement("w:pBdr")
|
|
p_pr.append(p_bdr)
|
|
bottom = OxmlElement("w:bottom")
|
|
bottom.set(qn("w:val"), "single")
|
|
bottom.set(qn("w:sz"), size)
|
|
bottom.set(qn("w:space"), "1")
|
|
bottom.set(qn("w:color"), color)
|
|
p_bdr.append(bottom)
|
|
|
|
|
|
def add_top_border(paragraph, color=BORDER, size="6"):
|
|
p_pr = paragraph._p.get_or_add_pPr()
|
|
p_bdr = p_pr.find(qn("w:pBdr"))
|
|
if p_bdr is None:
|
|
p_bdr = OxmlElement("w:pBdr")
|
|
p_pr.append(p_bdr)
|
|
top = OxmlElement("w:top")
|
|
top.set(qn("w:val"), "single")
|
|
top.set(qn("w:sz"), size)
|
|
top.set(qn("w:space"), "1")
|
|
top.set(qn("w:color"), color)
|
|
p_bdr.append(top)
|
|
|
|
|
|
def add_left_border(paragraph, color=BLUE, size="18"):
|
|
p_pr = paragraph._p.get_or_add_pPr()
|
|
p_bdr = p_pr.find(qn("w:pBdr"))
|
|
if p_bdr is None:
|
|
p_bdr = OxmlElement("w:pBdr")
|
|
p_pr.append(p_bdr)
|
|
left = OxmlElement("w:left")
|
|
left.set(qn("w:val"), "single")
|
|
left.set(qn("w:sz"), size)
|
|
left.set(qn("w:space"), "8")
|
|
left.set(qn("w:color"), color)
|
|
p_bdr.append(left)
|
|
|
|
|
|
def configure_styles(doc):
|
|
normal = doc.styles["Normal"]
|
|
normal.font.name = FONT_BODY
|
|
normal._element.rPr.rFonts.set(qn("w:eastAsia"), FONT_BODY)
|
|
normal.font.size = Pt(11)
|
|
normal.font.color.rgb = RGBColor.from_string(INK)
|
|
normal.paragraph_format.space_before = Pt(0)
|
|
normal.paragraph_format.space_after = Pt(6)
|
|
normal.paragraph_format.line_spacing = 1.25
|
|
|
|
specs = {
|
|
"Title": (30, NAVY, 0, 8),
|
|
"Subtitle": (13.5, MUTED, 0, 18),
|
|
"Heading 1": (16, NAVY, 18, 10),
|
|
"Heading 2": (13, BLUE, 14, 7),
|
|
"Heading 3": (12, DARK_BLUE, 10, 5),
|
|
}
|
|
for style_name, (size, color, before, after) in specs.items():
|
|
style = doc.styles[style_name]
|
|
style.font.name = FONT_BODY
|
|
style._element.rPr.rFonts.set(qn("w:eastAsia"), FONT_BODY)
|
|
style.font.size = Pt(size)
|
|
style.font.color.rgb = RGBColor.from_string(color)
|
|
style.font.bold = style_name != "Subtitle"
|
|
style.paragraph_format.space_before = Pt(before)
|
|
style.paragraph_format.space_after = Pt(after)
|
|
style.paragraph_format.keep_with_next = True
|
|
|
|
for style_name in ("List Bullet", "List Number"):
|
|
style = doc.styles[style_name]
|
|
style.font.name = FONT_BODY
|
|
style._element.rPr.rFonts.set(qn("w:eastAsia"), FONT_BODY)
|
|
style.font.size = Pt(11)
|
|
style.paragraph_format.left_indent = Inches(0.375)
|
|
style.paragraph_format.first_line_indent = Inches(-0.188)
|
|
style.paragraph_format.space_after = Pt(4)
|
|
style.paragraph_format.line_spacing = 1.25
|
|
|
|
|
|
def configure_section_geometry(section):
|
|
section.page_width = Inches(8.5)
|
|
section.page_height = Inches(11)
|
|
section.top_margin = Inches(1)
|
|
section.bottom_margin = Inches(1)
|
|
section.left_margin = Inches(1)
|
|
section.right_margin = Inches(1)
|
|
section.header_distance = Inches(0.492)
|
|
section.footer_distance = Inches(0.492)
|
|
|
|
|
|
def clear_paragraph(paragraph):
|
|
for child in list(paragraph._p):
|
|
if child.tag != qn("w:pPr"):
|
|
paragraph._p.remove(child)
|
|
|
|
|
|
def set_page_number_start(section, start=1):
|
|
sect_pr = section._sectPr
|
|
page_num = sect_pr.find(qn("w:pgNumType"))
|
|
if page_num is None:
|
|
page_num = OxmlElement("w:pgNumType")
|
|
sect_pr.append(page_num)
|
|
page_num.set(qn("w:start"), str(start))
|
|
|
|
|
|
def configure_cover_section(section):
|
|
configure_section_geometry(section)
|
|
section.header.is_linked_to_previous = False
|
|
section.footer.is_linked_to_previous = False
|
|
clear_paragraph(section.header.paragraphs[0])
|
|
clear_paragraph(section.footer.paragraphs[0])
|
|
|
|
|
|
def configure_content_section(section, short_title):
|
|
configure_section_geometry(section)
|
|
section.header.is_linked_to_previous = False
|
|
section.footer.is_linked_to_previous = False
|
|
set_page_number_start(section, 1)
|
|
|
|
header_p = section.header.paragraphs[0]
|
|
clear_paragraph(header_p)
|
|
header_p.paragraph_format.space_after = Pt(4)
|
|
header_p.paragraph_format.tab_stops.add_tab_stop(Inches(6.5))
|
|
left = header_p.add_run(short_title)
|
|
set_run_font(left, size=8.5, color=NAVY, bold=True)
|
|
right = header_p.add_run("\tAX HUB · TOOL DEVELOPMENT STANDARD")
|
|
set_run_font(right, size=8.2, color=MUTED)
|
|
add_bottom_border(header_p, "D7DEE8", "4")
|
|
|
|
footer_p = section.footer.paragraphs[0]
|
|
clear_paragraph(footer_p)
|
|
footer_p.paragraph_format.tab_stops.add_tab_stop(Inches(6.5))
|
|
left = footer_p.add_run("INTERNAL USE · TOOL POD DEVELOPMENT")
|
|
set_run_font(left, size=8.2, color=MUTED)
|
|
right = footer_p.add_run("\t")
|
|
set_run_font(right, size=8.2, color=MUTED)
|
|
add_page_number(footer_p)
|
|
|
|
|
|
def add_inline(paragraph, text, default_size=11, default_color=None):
|
|
token_pattern = re.compile(r"(\*\*.+?\*\*|`[^`]+`|\[[^\]]+\]\([^)]+\))")
|
|
cursor = 0
|
|
for match in token_pattern.finditer(text):
|
|
if match.start() > cursor:
|
|
run = paragraph.add_run(text[cursor:match.start()])
|
|
set_run_font(run, size=default_size, color=default_color)
|
|
token = match.group(0)
|
|
if token.startswith("**"):
|
|
run = paragraph.add_run(token[2:-2])
|
|
set_run_font(run, size=default_size, color=default_color, bold=True)
|
|
elif token.startswith("`"):
|
|
run = paragraph.add_run(token[1:-1])
|
|
set_run_font(run, name=FONT_CODE, size=max(8.5, default_size - 1), color=DARK_BLUE)
|
|
shd = OxmlElement("w:shd")
|
|
shd.set(qn("w:fill"), "EEF2F6")
|
|
run._r.get_or_add_rPr().append(shd)
|
|
else:
|
|
label, url = re.match(r"\[([^\]]+)\]\(([^)]+)\)", token).groups()
|
|
run = paragraph.add_run(label)
|
|
set_run_font(run, size=default_size, color=BLUE)
|
|
run.underline = True
|
|
cursor = match.end()
|
|
if cursor < len(text):
|
|
run = paragraph.add_run(text[cursor:])
|
|
set_run_font(run, size=default_size, color=default_color)
|
|
|
|
|
|
def add_cover(doc, title, subtitle, doc_type, highlights):
|
|
p = doc.add_paragraph()
|
|
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
p.paragraph_format.space_before = Pt(88)
|
|
p.paragraph_format.space_after = Pt(20)
|
|
r = p.add_run("AX HUB | TOOL DEVELOPMENT STANDARD")
|
|
set_run_font(r, size=9.5, color=GOLD, bold=True)
|
|
|
|
title_p = doc.add_paragraph(style="Title")
|
|
title_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
title_p.paragraph_format.space_after = Pt(10)
|
|
add_inline(title_p, title, default_size=30, default_color=NAVY)
|
|
|
|
subtitle_p = doc.add_paragraph(style="Subtitle")
|
|
subtitle_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
add_inline(subtitle_p, subtitle, default_size=13.5, default_color=DARK_BLUE)
|
|
|
|
rule = doc.add_paragraph()
|
|
rule.paragraph_format.left_indent = Inches(1.55)
|
|
rule.paragraph_format.right_indent = Inches(1.55)
|
|
rule.paragraph_format.space_after = Pt(24)
|
|
add_bottom_border(rule, GOLD, "10")
|
|
|
|
focus = doc.add_paragraph()
|
|
focus.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
focus.paragraph_format.space_after = Pt(86)
|
|
focus_run = focus.add_run(" · ".join(highlights))
|
|
set_run_font(focus_run, size=10.5, color=BLUE, bold=True)
|
|
|
|
rows = [
|
|
("문서 구분", doc_type),
|
|
("기준 버전", "1차본 | 2026-08-14"),
|
|
("적용 범위", "dap-was-lib 및 Tool Pod (CUS · SAL · PRO · SYS)"),
|
|
]
|
|
for label, value in rows:
|
|
p = doc.add_paragraph()
|
|
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
p.paragraph_format.first_line_indent = Inches(0)
|
|
p.paragraph_format.space_after = Pt(5)
|
|
label_run = p.add_run(f"{label} | ")
|
|
set_run_font(label_run, size=9.2, color=MUTED, bold=True)
|
|
value_run = p.add_run(value)
|
|
set_run_font(value_run, size=9.5, color=INK)
|
|
|
|
|
|
def extract_section_titles(markdown_text):
|
|
return [
|
|
line.removeprefix("## ").strip()
|
|
for line in markdown_text.splitlines()
|
|
if line.startswith("## ")
|
|
]
|
|
|
|
|
|
def add_document_overview(doc, subtitle, markdown_text):
|
|
heading = doc.add_heading("문서 안내", level=1)
|
|
add_bottom_border(heading, LIGHT_BLUE, "8")
|
|
|
|
lead = doc.add_paragraph()
|
|
lead.paragraph_format.space_after = Pt(10)
|
|
run = lead.add_run(subtitle)
|
|
set_run_font(run, size=11.5, color=NAVY, bold=True)
|
|
|
|
p = doc.add_paragraph()
|
|
p.paragraph_format.left_indent = Inches(0.12)
|
|
p.paragraph_format.right_indent = Inches(0.12)
|
|
p.paragraph_format.space_before = Pt(2)
|
|
p.paragraph_format.space_after = Pt(12)
|
|
p_pr = p._p.get_or_add_pPr()
|
|
shd = OxmlElement("w:shd")
|
|
shd.set(qn("w:fill"), SOFT_BLUE)
|
|
p_pr.append(shd)
|
|
add_left_border(p, BLUE, "20")
|
|
marker = p.add_run("READING GUIDE ")
|
|
set_run_font(marker, size=9, color=BLUE, bold=True)
|
|
body = p.add_run("이 문서는 Tool Pod 개발자가 설계·구현·검증 과정에서 바로 참고할 수 있도록 현재 소스 기준으로 정리했습니다.")
|
|
set_run_font(body, size=10.2, color=INK)
|
|
|
|
section_heading = doc.add_heading("주요 구성", level=2)
|
|
section_heading.paragraph_format.space_after = Pt(6)
|
|
for title in extract_section_titles(markdown_text):
|
|
item = doc.add_paragraph(style="List Bullet")
|
|
add_inline(item, title, default_size=10.2, default_color=INK)
|
|
|
|
rule = doc.add_paragraph()
|
|
rule.paragraph_format.space_before = Pt(6)
|
|
rule.paragraph_format.space_after = Pt(4)
|
|
add_bottom_border(rule, "D7DEE8", "4")
|
|
|
|
|
|
def is_table_separator(line):
|
|
cells = [c.strip() for c in line.strip().strip("|").split("|")]
|
|
return bool(cells) and all(re.fullmatch(r":?-{3,}:?", cell or "") for cell in cells)
|
|
|
|
|
|
def parse_table(lines, start):
|
|
rows = []
|
|
idx = start
|
|
while idx < len(lines) and lines[idx].strip().startswith("|"):
|
|
if not is_table_separator(lines[idx]):
|
|
rows.append([c.strip() for c in lines[idx].strip().strip("|").split("|")])
|
|
idx += 1
|
|
return rows, idx
|
|
|
|
|
|
def table_widths(rows):
|
|
col_count = max(len(row) for row in rows)
|
|
weights = []
|
|
for col in range(col_count):
|
|
max_len = max(len(row[col]) if col < len(row) else 0 for row in rows)
|
|
weights.append(max(8, min(max_len, 48)))
|
|
total = sum(weights)
|
|
widths = [max(900, round(CONTENT_DXA * weight / total)) for weight in weights]
|
|
scale = CONTENT_DXA / sum(widths)
|
|
widths = [round(width * scale) for width in widths]
|
|
widths[-1] += CONTENT_DXA - sum(widths)
|
|
return widths
|
|
|
|
|
|
def add_markdown_table(doc, rows):
|
|
if not rows:
|
|
return
|
|
col_count = max(len(row) for row in rows)
|
|
table = doc.add_table(rows=len(rows), cols=col_count)
|
|
table.style = "Table Grid"
|
|
for row_idx, values in enumerate(rows):
|
|
for col_idx in range(col_count):
|
|
cell = table.cell(row_idx, col_idx)
|
|
cell.text = ""
|
|
p = cell.paragraphs[0]
|
|
p.paragraph_format.space_before = Pt(0)
|
|
p.paragraph_format.space_after = Pt(0)
|
|
p.paragraph_format.line_spacing = 1.15
|
|
value = values[col_idx] if col_idx < len(values) else ""
|
|
add_inline(p, value, default_size=8.8 if col_count >= 4 else 9.2)
|
|
if row_idx == 0:
|
|
set_cell_shading(cell, NAVY)
|
|
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
for run in p.runs:
|
|
run.bold = True
|
|
run.font.color.rgb = RGBColor.from_string(WHITE)
|
|
elif row_idx % 2 == 0:
|
|
set_cell_shading(cell, TABLE_ALT)
|
|
if row_idx > 0:
|
|
column_values = [row[col_idx] if col_idx < len(row) else "" for row in rows[1:]]
|
|
if column_values and max(len(value) for value in column_values) <= 12:
|
|
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
repeat_table_header(table.rows[0])
|
|
set_table_geometry(table, table_widths(rows))
|
|
after = doc.add_paragraph()
|
|
after.paragraph_format.space_after = Pt(2)
|
|
|
|
|
|
def add_code_block(doc, code_lines):
|
|
if not code_lines:
|
|
return
|
|
p = doc.add_paragraph()
|
|
p.paragraph_format.left_indent = Inches(0.12)
|
|
p.paragraph_format.right_indent = Inches(0.12)
|
|
p.paragraph_format.space_before = Pt(3)
|
|
p.paragraph_format.space_after = Pt(8)
|
|
p.paragraph_format.line_spacing = 1.0
|
|
p.paragraph_format.keep_together = len(code_lines) <= 12
|
|
p_pr = p._p.get_or_add_pPr()
|
|
shd = OxmlElement("w:shd")
|
|
shd.set(qn("w:fill"), "F5F7FA")
|
|
p_pr.append(shd)
|
|
add_left_border(p, BLUE, "16")
|
|
for index, line in enumerate(code_lines):
|
|
run = p.add_run(line)
|
|
set_run_font(run, name=FONT_CODE, size=8.2, color=NAVY)
|
|
if index < len(code_lines) - 1:
|
|
run.add_break()
|
|
|
|
|
|
def add_callout(doc, text):
|
|
p = doc.add_paragraph()
|
|
p.paragraph_format.left_indent = Inches(0.12)
|
|
p.paragraph_format.right_indent = Inches(0.12)
|
|
p.paragraph_format.space_before = Pt(3)
|
|
p.paragraph_format.space_after = Pt(8)
|
|
p_pr = p._p.get_or_add_pPr()
|
|
shd = OxmlElement("w:shd")
|
|
shd.set(qn("w:fill"), SOFT_BLUE)
|
|
p_pr.append(shd)
|
|
add_left_border(p)
|
|
marker = p.add_run("NOTE ")
|
|
set_run_font(marker, size=9.5, color=BLUE, bold=True)
|
|
add_inline(p, text, default_size=10)
|
|
|
|
|
|
def add_markdown_body(doc, markdown_text):
|
|
lines = markdown_text.splitlines()
|
|
first_h1_seen = False
|
|
in_code = False
|
|
code_lines = []
|
|
idx = 0
|
|
while idx < len(lines):
|
|
raw = lines[idx]
|
|
line = raw.rstrip()
|
|
stripped = line.strip()
|
|
|
|
if stripped.startswith("```"):
|
|
if in_code:
|
|
add_code_block(doc, code_lines)
|
|
code_lines = []
|
|
in_code = False
|
|
else:
|
|
in_code = True
|
|
idx += 1
|
|
continue
|
|
if in_code:
|
|
code_lines.append(line)
|
|
idx += 1
|
|
continue
|
|
if not stripped:
|
|
idx += 1
|
|
continue
|
|
if stripped.startswith("# ") and not first_h1_seen:
|
|
first_h1_seen = True
|
|
idx += 1
|
|
continue
|
|
if stripped.startswith("### "):
|
|
doc.add_heading(stripped[4:], level=2)
|
|
idx += 1
|
|
continue
|
|
if stripped.startswith("## "):
|
|
heading = doc.add_heading(stripped[3:], level=1)
|
|
add_bottom_border(heading, LIGHT_BLUE, "8")
|
|
idx += 1
|
|
continue
|
|
if stripped.startswith("# "):
|
|
heading = doc.add_heading(stripped[2:], level=1)
|
|
add_bottom_border(heading, LIGHT_BLUE, "8")
|
|
idx += 1
|
|
continue
|
|
if stripped.startswith("|") and idx + 1 < len(lines) and is_table_separator(lines[idx + 1]):
|
|
rows, idx = parse_table(lines, idx)
|
|
add_markdown_table(doc, rows)
|
|
continue
|
|
if stripped.startswith("> "):
|
|
add_callout(doc, stripped[2:])
|
|
idx += 1
|
|
continue
|
|
match_ordered = re.match(r"^\d+\.\s+(.*)$", stripped)
|
|
if match_ordered:
|
|
p = doc.add_paragraph(style="List Number")
|
|
add_inline(p, match_ordered.group(1))
|
|
idx += 1
|
|
continue
|
|
if stripped.startswith("- [ ] "):
|
|
p = doc.add_paragraph(style="List Bullet")
|
|
add_inline(p, "☐ " + stripped[6:])
|
|
idx += 1
|
|
continue
|
|
if stripped.startswith("- "):
|
|
p = doc.add_paragraph(style="List Bullet")
|
|
add_inline(p, stripped[2:])
|
|
idx += 1
|
|
continue
|
|
p = doc.add_paragraph()
|
|
add_inline(p, stripped)
|
|
idx += 1
|
|
if code_lines:
|
|
add_code_block(doc, code_lines)
|
|
|
|
|
|
def set_document_properties(doc, title):
|
|
props = doc.core_properties
|
|
props.title = title
|
|
props.subject = "AX HUB Tool 개발 표준"
|
|
props.author = "AX HUB MCP & TOOL"
|
|
props.keywords = "AX HUB, Tool Pod, MCP, Glow, MCI, HTTP"
|
|
props.comments = "Tool Pod 개발자용 문서"
|
|
|
|
|
|
def build(source_path, short_title, subtitle, highlights):
|
|
markdown = source_path.read_text(encoding="utf-8")
|
|
first_line = markdown.splitlines()[0]
|
|
full_title = first_line.removeprefix("# ").strip()
|
|
|
|
doc = Document()
|
|
configure_styles(doc)
|
|
set_document_properties(doc, full_title)
|
|
configure_cover_section(doc.sections[0])
|
|
add_cover(doc, full_title, subtitle, short_title, highlights)
|
|
|
|
content_section = doc.add_section(WD_SECTION.NEW_PAGE)
|
|
configure_content_section(content_section, short_title)
|
|
add_document_overview(doc, subtitle, markdown)
|
|
add_markdown_body(doc, markdown)
|
|
|
|
output = OUTPUT_DIR / f"{source_path.stem}.docx"
|
|
doc.save(output)
|
|
return output
|
|
|
|
|
|
def main():
|
|
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
for source, short_title, subtitle, highlights in SOURCES:
|
|
output = build(source, short_title, subtitle, highlights)
|
|
print(output)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|