refactor(desktop): session-scoped voice runtime, STT/LLM ports, caption ownership and meeting export fixes
This commit is contained in:
parent
0adedf5e7b
commit
83cf9133ac
39 changed files with 4787 additions and 1061 deletions
|
|
@ -1,31 +1,90 @@
|
|||
// src/main/utils/markdown-to-docx.ts
|
||||
// Phase 14.5: 마크다운 → DOCX 변환 유틸리티
|
||||
// packages/core/src/utils/markdown-to-docx.ts
|
||||
// Phase 14.5: 마크다운 → DOCX 변환 유틸리티.
|
||||
// 마크다운 해석은 meeting-markdown 의 parseMarkdownBlocks 가 단일 정본이다 —
|
||||
// 여기서는 블록을 docx 요소로 렌더링만 한다(docx 의존은 이 서브패스에만 격리).
|
||||
|
||||
import { Document, Packer, Paragraph, TextRun, HeadingLevel } from 'docx'
|
||||
import {
|
||||
Document,
|
||||
Packer,
|
||||
Paragraph,
|
||||
Table,
|
||||
TableCell,
|
||||
TableRow,
|
||||
TextRun,
|
||||
HeadingLevel,
|
||||
WidthType,
|
||||
} from 'docx'
|
||||
import {
|
||||
CHECKBOX_CHECKED,
|
||||
CHECKBOX_UNCHECKED,
|
||||
parseMarkdownBlocks,
|
||||
type MarkdownBlock,
|
||||
} from './meeting-markdown'
|
||||
|
||||
export async function markdownToDocx(markdown: string, title: string): Promise<Buffer> {
|
||||
const paragraphs: Paragraph[] = []
|
||||
const HEADING_LEVELS = {
|
||||
1: HeadingLevel.HEADING_1,
|
||||
2: HeadingLevel.HEADING_2,
|
||||
3: HeadingLevel.HEADING_3,
|
||||
} as const
|
||||
|
||||
// 타이틀 추가
|
||||
paragraphs.push(new Paragraph({ text: title, heading: HeadingLevel.TITLE }))
|
||||
function tableCell(text: string, bold: boolean): TableCell {
|
||||
return new TableCell({
|
||||
children: [new Paragraph({ children: [new TextRun({ text, bold })] })],
|
||||
})
|
||||
}
|
||||
|
||||
// 줄 단위 파싱
|
||||
for (const line of markdown.split('\n')) {
|
||||
if (line.startsWith('## ')) {
|
||||
paragraphs.push(new Paragraph({ text: line.slice(3), heading: HeadingLevel.HEADING_2 }))
|
||||
} else if (line.startsWith('### ')) {
|
||||
paragraphs.push(new Paragraph({ text: line.slice(4), heading: HeadingLevel.HEADING_3 }))
|
||||
} else if (line.startsWith('# ')) {
|
||||
paragraphs.push(new Paragraph({ text: line.slice(2), heading: HeadingLevel.HEADING_1 }))
|
||||
} else if (line.startsWith('- [ ] ')) {
|
||||
paragraphs.push(new Paragraph({ text: `\u2610 ${line.slice(6)}`, bullet: { level: 0 } }))
|
||||
} else if (line.startsWith('- ') || line.startsWith('* ')) {
|
||||
paragraphs.push(new Paragraph({ text: line.slice(2), bullet: { level: 0 } }))
|
||||
} else if (line.trim()) {
|
||||
paragraphs.push(new Paragraph({ children: [new TextRun(line)] }))
|
||||
/** 블록을 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
|
||||
}
|
||||
|
||||
const doc = new Document({ sections: [{ children: paragraphs }] })
|
||||
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))
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue