d3ro-voice/packages/core/src/utils/markdown-to-docx.ts

90 lines
2.9 KiB
TypeScript

// packages/core/src/utils/markdown-to-docx.ts
// Phase 14.5: 마크다운 → DOCX 변환 유틸리티.
// 마크다운 해석은 meeting-markdown 의 parseMarkdownBlocks 가 단일 정본이다 —
// 여기서는 블록을 docx 요소로 렌더링만 한다(docx 의존은 이 서브패스에만 격리).
import {
Document,
Packer,
Paragraph,
Table,
TableCell,
TableRow,
TextRun,
HeadingLevel,
WidthType,
} from 'docx'
import {
CHECKBOX_CHECKED,
CHECKBOX_UNCHECKED,
parseMarkdownBlocks,
type MarkdownBlock,
} from './meeting-markdown'
const HEADING_LEVELS = {
1: HeadingLevel.HEADING_1,
2: HeadingLevel.HEADING_2,
3: HeadingLevel.HEADING_3,
} as const
function tableCell(text: string, bold: boolean): TableCell {
return new TableCell({
children: [new Paragraph({ children: [new TextRun({ text, bold })] })],
})
}
/** 블록을 docx 문서 요소(문단/표)로 바꾼다. 순수 매핑이라 개수·종류로 검증할 수 있다. */
export function blocksToDocxElements(blocks: readonly MarkdownBlock[]): Array<Paragraph | Table> {
const elements: Array<Paragraph | Table> = []
for (const block of blocks) {
switch (block.type) {
case 'heading':
elements.push(new Paragraph({ text: block.text, heading: HEADING_LEVELS[block.level] }))
break
case 'bullet':
elements.push(new Paragraph({ text: block.text, bullet: { level: 0 } }))
break
case 'checkbox':
elements.push(
new Paragraph({
text: `${block.checked ? CHECKBOX_CHECKED : CHECKBOX_UNCHECKED} ${block.text}`,
bullet: { level: 0 },
}),
)
break
case 'paragraph':
// 줄마다 문단 하나 — 한 줄짜리 항목이 한 문단으로 합쳐지지 않게 한다
for (const line of block.lines) {
elements.push(new Paragraph({ children: [new TextRun(line)] }))
}
break
case 'table': {
const rows: TableRow[] = []
if (block.header.length > 0) {
rows.push(new TableRow({ children: block.header.map((c) => tableCell(c, true)), tableHeader: true }))
}
for (const row of block.rows) {
rows.push(new TableRow({ children: row.map((c) => tableCell(c, false)) }))
}
if (rows.length > 0) {
elements.push(new Table({ rows, width: { size: 100, type: WidthType.PERCENTAGE } }))
}
break
}
case 'rule':
// 구분선은 문서 흐름상 빈 문단으로 둔다
elements.push(new Paragraph({ text: '' }))
break
}
}
return elements
}
export async function markdownToDocx(markdown: string, title: string): Promise<Buffer> {
const children: Array<Paragraph | Table> = [
new Paragraph({ text: title, heading: HeadingLevel.TITLE }),
...blocksToDocxElements(parseMarkdownBlocks(markdown)),
]
const doc = new Document({ sections: [{ children }] })
return Buffer.from(await Packer.toBuffer(doc))
}