90 lines
2.9 KiB
TypeScript
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))
|
|
}
|