AI_Formatter_V02 / src /utils /docxExporter.js
Varun10000's picture
Upload 28 files
63bca44 verified
Raw
History Blame Contribute Delete
12.3 kB
import { Document, Packer, Paragraph, TextRun, HeadingLevel, Table, TableRow, TableCell, WidthType, ShadingType, AlignmentType, BorderStyle, Header, ImageRun } from 'docx';
import { saveAs } from 'file-saver';
/**
* Exports structured JSON content to a .docx file.
* @param {Array} elements - The structured document elements.
* @param {string} filename - The name of the file to save.
*/
/**
* Parses markdown-style text (bold/italic) into docx TextRun objects.
* @param {string} text
* @returns {TextRun[]}
*/
const parseRichText = (text, options = {}) => {
const defaultOptions = { font: "Times New Roman", size: 22, ...options };
if (typeof text !== 'string') return [new TextRun({ text: String(text), ...defaultOptions })];
const parts = [];
const regex = /(\*\*.*?\*\*|\*.*?\*)/g;
let lastIndex = 0;
let match;
while ((match = regex.exec(text)) !== null) {
if (match.index > lastIndex) {
parts.push(new TextRun({ text: text.substring(lastIndex, match.index), ...defaultOptions }));
}
const content = match[0];
if (content.startsWith('**')) {
parts.push(new TextRun({ text: content.slice(2, -2), bold: true, ...defaultOptions }));
} else if (content.startsWith('*')) {
parts.push(new TextRun({ text: content.slice(1, -1), italics: true, ...defaultOptions }));
}
lastIndex = regex.lastIndex;
}
if (lastIndex < text.length) {
parts.push(new TextRun({ text: text.substring(lastIndex), ...defaultOptions }));
}
return parts.length > 0 ? parts : [new TextRun({ text, ...defaultOptions })];
};
const stripPrefix = (text) => {
if (typeof text !== 'string') return text;
return text.replace(/^(\([a-zA-Z0-9]+\)|[a-zA-Z0-9]+[\.\)]|[\-\•])\s*/, '');
};
export const exportToDocx = async (elements, filename = 'Formatted_Document.docx', docType = 'rfp', logoBase64 = null, contractAddress = '') => {
const isRfp = docType === 'rfp';
const children = elements.map(el => {
const align = el.alignment === 'center' ? AlignmentType.CENTER : AlignmentType.LEFT;
switch (el.type) {
case 'heading1':
return new Paragraph({
children: parseRichText(el.content, { color: '000000', bold: true, size: 22 }),
shading: isRfp ? {
fill: 'FFC000',
type: ShadingType.CLEAR,
color: 'auto',
} : undefined,
spacing: {
before: isRfp ? 240 : 480,
after: isRfp ? 200 : 320,
},
alignment: align,
});
case 'heading2':
return new Paragraph({
children: parseRichText(el.content, { size: 22, bold: true, color: '000000' }),
spacing: {
before: isRfp ? 200 : 400,
after: isRfp ? 160 : 240
},
alignment: align,
});
case 'heading3':
return new Paragraph({
children: parseRichText(el.content, { size: 22, bold: true, italics: true, color: '000000' }),
spacing: {
before: isRfp ? 160 : 320,
after: isRfp ? 120 : 200
},
alignment: align,
});
case 'bulletList':
return el.content.map(item => new Paragraph({
children: parseRichText(stripPrefix(item)),
bullet: { level: 0 },
indent: { left: 720, hanging: 360 },
spacing: {
before: isRfp ? 40 : 80,
after: isRfp ? 40 : 80
},
alignment: align,
}));
case 'numberedList':
return el.content.map(item => new Paragraph({
children: parseRichText(stripPrefix(item)),
numbering: {
reference: 'main-numbering',
level: el.listType === 'lowerAlpha' ? 1 : 0
},
indent: {
left: el.listType === 'lowerAlpha' ? 1440 : 720,
hanging: 360
},
spacing: {
before: isRfp ? 40 : 80,
after: isRfp ? 40 : 80
},
alignment: align,
}));
case 'table':
return new Table({
width: {
size: 100,
type: WidthType.PERCENTAGE,
},
rows: el.content.map(row =>
new TableRow({
children: row.map(cell =>
new TableCell({
children: [new Paragraph({
children: parseRichText(cell, { size: 22 }),
spacing: { line: 360 } // 1.5 line spacing
})],
borders: {
top: { style: BorderStyle.SINGLE, size: 2 },
bottom: { style: BorderStyle.SINGLE, size: 2 },
left: { style: BorderStyle.SINGLE, size: 2 },
right: { style: BorderStyle.SINGLE, size: 2 },
},
margins: {
top: 100,
bottom: 100,
left: 100,
right: 100,
},
shading: {
fill: 'FFFFFF',
type: ShadingType.CLEAR,
color: 'auto',
}
})
)
})
),
});
case 'paragraph':
default:
return new Paragraph({
children: parseRichText(el.content),
spacing: {
before: isRfp ? 120 : 240,
after: isRfp ? 120 : 240,
line: 360 // 1.5 line spacing
},
alignment: align,
});
}
}).flat();
const doc = new Document({
numbering: {
config: [
{
reference: "main-numbering",
levels: [
{
level: 0,
format: "decimal",
text: "%1.",
alignment: AlignmentType.LEFT,
style: {
paragraph: {
indent: { left: 720, hanging: 360 } /* 0.5" left, 0.25" hanging */
}
}
},
{
level: 1,
format: "lowerLetter",
text: "%2.",
alignment: AlignmentType.LEFT,
style: {
paragraph: {
indent: { left: 1440, hanging: 360 } /* 1.0" left, 0.25" hanging */
}
}
},
],
},
],
},
sections: [{
properties: {},
headers: {
default: new Header({
children: [
new Table({
width: {
size: 100,
type: WidthType.PERCENTAGE,
},
borders: {
top: { style: BorderStyle.NONE },
bottom: { style: BorderStyle.NONE },
left: { style: BorderStyle.NONE },
right: { style: BorderStyle.NONE },
insideVertical: { style: BorderStyle.NONE },
insideHorizontal: { style: BorderStyle.NONE },
},
rows: [
new TableRow({
children: [
new TableCell({
width: { size: 35, type: WidthType.PERCENTAGE },
children: logoBase64 ? [
new Paragraph({
children: [
new ImageRun({
data: Uint8Array.from(atob(logoBase64.split(',')[1]), c => c.charCodeAt(0)),
transformation: {
width: 150,
height: 50,
},
}),
],
alignment: AlignmentType.LEFT,
})
] : [new Paragraph({})], // Fallback for empty cell
}),
new TableCell({
width: { size: 65, type: WidthType.PERCENTAGE },
verticalAlign: AlignmentType.CENTER,
children: contractAddress ? [
new Paragraph({
children: contractAddress.split('\n').map((line, i) =>
new TextRun({
text: line,
font: "Times New Roman",
size: 20, // 10pt
color: "000000",
break: i > 0 ? 1 : 0
})
),
alignment: AlignmentType.RIGHT,
})
] : [new Paragraph({})], // Fallback for empty cell
}),
],
}),
],
}),
],
}),
},
children: children,
}],
});
const blob = await Packer.toBlob(doc);
saveAs(blob, filename);
};