| |
| |
| |
| |
|
|
| import { |
| Document, |
| Paragraph, |
| TextRun, |
| HeadingLevel, |
| Table, |
| TableRow, |
| TableCell, |
| WidthType, |
| BorderStyle, |
| Packer, |
| AlignmentType |
| } from "docx"; |
|
|
| |
| |
| |
| function parseMarkdown(markdown) { |
| const lines = markdown.split("\n"); |
| const elements = []; |
| let inCodeBlock = false; |
| let codeContent = []; |
| let inTable = false; |
| let tableRows = []; |
| let listItems = []; |
| let listType = null; |
|
|
| const flushList = () => { |
| if (listItems.length > 0) { |
| elements.push({ |
| type: listType === "ol" ? "orderedList" : "bulletList", |
| items: [...listItems] |
| }); |
| listItems = []; |
| listType = null; |
| } |
| }; |
|
|
| const flushTable = () => { |
| if (tableRows.length > 0) { |
| elements.push({ |
| type: "table", |
| rows: [...tableRows] |
| }); |
| tableRows = []; |
| inTable = false; |
| } |
| }; |
|
|
| for (let i = 0; i < lines.length; i++) { |
| const line = lines[i]; |
|
|
| |
| if (line.startsWith("```")) { |
| if (inCodeBlock) { |
| elements.push({ |
| type: "codeBlock", |
| content: codeContent.join("\n") |
| }); |
| codeContent = []; |
| inCodeBlock = false; |
| } else { |
| flushList(); |
| flushTable(); |
| inCodeBlock = true; |
| } |
| continue; |
| } |
|
|
| if (inCodeBlock) { |
| codeContent.push(line); |
| continue; |
| } |
|
|
| |
| if (line.includes("|") && line.trim().startsWith("|")) { |
| flushList(); |
| const cells = line.split("|").slice(1, -1).map(c => c.trim()); |
|
|
| |
| if (cells.every(c => /^[-:]+$/.test(c))) { |
| continue; |
| } |
|
|
| tableRows.push(cells); |
| inTable = true; |
| continue; |
| } else if (inTable) { |
| flushTable(); |
| } |
|
|
| |
| if (!line.trim()) { |
| flushList(); |
| continue; |
| } |
|
|
| |
| const headingMatch = line.match(/^(#{1,6})\s+(.+)$/); |
| if (headingMatch) { |
| flushList(); |
| elements.push({ |
| type: "heading", |
| level: headingMatch[1].length, |
| content: headingMatch[2] |
| }); |
| continue; |
| } |
|
|
| |
| if (/^[-*_]{3,}$/.test(line.trim())) { |
| flushList(); |
| elements.push({ type: "hr" }); |
| continue; |
| } |
|
|
| |
| if (line.startsWith(">")) { |
| flushList(); |
| elements.push({ |
| type: "blockquote", |
| content: line.replace(/^>\s*/, "") |
| }); |
| continue; |
| } |
|
|
| |
| const olMatch = line.match(/^(\d+)\.\s+(.+)$/); |
| if (olMatch) { |
| if (listType === "ul") flushList(); |
| listType = "ol"; |
| listItems.push(olMatch[2]); |
| continue; |
| } |
|
|
| |
| const ulMatch = line.match(/^[-*+]\s+(.+)$/); |
| if (ulMatch) { |
| if (listType === "ol") flushList(); |
| listType = "ul"; |
| listItems.push(ulMatch[1]); |
| continue; |
| } |
|
|
| |
| flushList(); |
| elements.push({ |
| type: "paragraph", |
| content: line |
| }); |
| } |
|
|
| |
| flushList(); |
| flushTable(); |
|
|
| return elements; |
| } |
|
|
| |
| |
| |
| function parseInlineFormatting(text) { |
| const runs = []; |
| let remaining = text; |
|
|
| while (remaining.length > 0) { |
| |
| let match = remaining.match(/^\*\*\*(.+?)\*\*\*/); |
| if (match) { |
| runs.push(new TextRun({ text: match[1], bold: true, italics: true, font: "Calibri" })); |
| remaining = remaining.slice(match[0].length); |
| continue; |
| } |
|
|
| |
| match = remaining.match(/^(\*\*|__)(.+?)(\*\*|__)/); |
| if (match) { |
| runs.push(new TextRun({ text: match[2], bold: true, font: "Calibri" })); |
| remaining = remaining.slice(match[0].length); |
| continue; |
| } |
|
|
| |
| match = remaining.match(/^(\*|_)(.+?)(\*|_)/); |
| if (match) { |
| runs.push(new TextRun({ text: match[2], italics: true, font: "Calibri" })); |
| remaining = remaining.slice(match[0].length); |
| continue; |
| } |
|
|
| |
| match = remaining.match(/^`([^`]+)`/); |
| if (match) { |
| runs.push(new TextRun({ |
| text: match[1], |
| font: "Consolas", |
| shading: { fill: "E8E8E8" } |
| })); |
| remaining = remaining.slice(match[0].length); |
| continue; |
| } |
|
|
| |
| match = remaining.match(/^\[([^\]]+)\]\(([^)]+)\)/); |
| if (match) { |
| runs.push(new TextRun({ |
| text: match[1], |
| color: "0563C1", |
| underline: {}, |
| font: "Calibri" |
| })); |
| remaining = remaining.slice(match[0].length); |
| continue; |
| } |
|
|
| |
| match = remaining.match(/^[^*_`\[]+/); |
| if (match) { |
| runs.push(new TextRun({ text: match[0], font: "Calibri" })); |
| remaining = remaining.slice(match[0].length); |
| continue; |
| } |
|
|
| |
| runs.push(new TextRun({ text: remaining[0], font: "Calibri" })); |
| remaining = remaining.slice(1); |
| } |
|
|
| return runs; |
| } |
|
|
| |
| |
| |
| function elementsToDocx(elements) { |
| const children = []; |
|
|
| for (const el of elements) { |
| switch (el.type) { |
| case "heading": |
| const headingLevels = { |
| 1: HeadingLevel.HEADING_1, |
| 2: HeadingLevel.HEADING_2, |
| 3: HeadingLevel.HEADING_3, |
| 4: HeadingLevel.HEADING_4, |
| 5: HeadingLevel.HEADING_5, |
| 6: HeadingLevel.HEADING_6 |
| }; |
| children.push( |
| new Paragraph({ |
| heading: headingLevels[el.level] || HeadingLevel.HEADING_1, |
| children: parseInlineFormatting(el.content), |
| spacing: { before: 280, after: 120 } |
| }) |
| ); |
| break; |
|
|
| case "paragraph": |
| children.push( |
| new Paragraph({ |
| children: parseInlineFormatting(el.content), |
| spacing: { after: 280, line: 276 } |
| }) |
| ); |
| break; |
|
|
| case "bulletList": |
| for (let i = 0; i < el.items.length; i++) { |
| children.push( |
| new Paragraph({ |
| bullet: { level: 0 }, |
| children: parseInlineFormatting(el.items[i]), |
| spacing: { after: i === el.items.length - 1 ? 200 : 60 } |
| }) |
| ); |
| } |
| break; |
|
|
| case "orderedList": |
| for (let idx = 0; idx < el.items.length; idx++) { |
| children.push( |
| new Paragraph({ |
| numbering: { reference: "default-numbering", level: 0 }, |
| children: parseInlineFormatting(el.items[idx]), |
| spacing: { after: idx === el.items.length - 1 ? 200 : 60 } |
| }) |
| ); |
| } |
| break; |
|
|
| case "codeBlock": |
| children.push( |
| new Paragraph({ |
| children: [ |
| new TextRun({ |
| text: el.content, |
| font: "Consolas", |
| size: 20 |
| }) |
| ], |
| shading: { fill: "F5F5F5" }, |
| spacing: { before: 200, after: 200 } |
| }) |
| ); |
| break; |
|
|
| case "blockquote": |
| children.push( |
| new Paragraph({ |
| children: parseInlineFormatting(el.content), |
| indent: { left: 720 }, |
| border: { |
| left: { style: BorderStyle.SINGLE, size: 24, color: "CCCCCC" } |
| }, |
| spacing: { before: 200, after: 200 } |
| }) |
| ); |
| break; |
|
|
| case "table": |
| const tableRows = el.rows.map((row, rowIdx) => |
| new TableRow({ |
| children: row.map(cell => |
| new TableCell({ |
| children: [new Paragraph({ children: parseInlineFormatting(cell) })], |
| shading: rowIdx === 0 ? { fill: "E8E8E8" } : undefined |
| }) |
| ) |
| }) |
| ); |
| children.push( |
| new Table({ |
| rows: tableRows, |
| width: { size: 100, type: WidthType.PERCENTAGE } |
| }) |
| ); |
| break; |
|
|
| case "hr": |
| children.push( |
| new Paragraph({ |
| border: { bottom: { style: BorderStyle.SINGLE, size: 6, color: "CCCCCC" } }, |
| spacing: { before: 400, after: 400 } |
| }) |
| ); |
| break; |
| } |
| } |
|
|
| return children; |
| } |
|
|
| |
| |
| |
| export async function exportToWord(markdown, filename = "document") { |
| const elements = parseMarkdown(markdown); |
| const children = elementsToDocx(elements); |
|
|
| const doc = new Document({ |
| numbering: { |
| config: [ |
| { |
| reference: "default-numbering", |
| levels: [ |
| { |
| level: 0, |
| format: "decimal", |
| text: "%1.", |
| alignment: AlignmentType.START |
| } |
| ] |
| } |
| ] |
| }, |
| sections: [ |
| { |
| children |
| } |
| ] |
| }); |
|
|
| const blob = await Packer.toBlob(doc); |
|
|
| |
| const url = URL.createObjectURL(blob); |
| const link = document.createElement("a"); |
| link.href = url; |
| link.download = `${filename.replace(/[^a-zA-Z0-9-_]/g, "_")}.docx`; |
| document.body.appendChild(link); |
| link.click(); |
| document.body.removeChild(link); |
| URL.revokeObjectURL(url); |
| } |
|
|