feat: implement MCI session fetching using employee number
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 1m3s
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 1m3s
This commit is contained in:
633
docs/tool-guide/build_tool_guides_docx.py
Normal file
633
docs/tool-guide/build_tool_guides_docx.py
Normal file
@@ -0,0 +1,633 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user