function onOpen() { const ui = SpreadsheetApp.getUi(); ui.createMenu('CMS Document Generation') .addItem('Generate document for current row', 'generateForCurrentRow') .addToUi(); } type DocSection = | GoogleAppsScript.Document.Body | GoogleAppsScript.Document.HeaderSection | GoogleAppsScript.Document.FooterSection; function copyElement( source_element: GoogleAppsScript.Document.Element, dest: DocSection, index?: number ) { const element = source_element.copy(); // based on https://stackoverflow.com/questions/6783819/google-app-script-copy-document-page if (element.getType() == DocumentApp.ElementType.PARAGRAPH) if (index !== undefined) dest.insertParagraph(index, element.asParagraph()); else dest.appendParagraph(element.asParagraph()); else if (element.getType() == DocumentApp.ElementType.TABLE) if (index !== undefined) dest.insertTable(index, element.asTable()); else dest.appendTable(element.asTable()); else if (element.getType() == DocumentApp.ElementType.LIST_ITEM) if (index !== undefined) dest.insertListItem(index, element.asListItem()); else dest.appendListItem(element.asListItem()); else if (element.getType() == DocumentApp.ElementType.INLINE_IMAGE) if (index !== undefined) dest.insertImage(index, element.asInlineImage()); else dest.appendImage(element.asInlineImage()); else throw new Error( "According to the doc this type couldn't appear in the body: " + element.getType() ); } function copySection(source: DocSection, dest: DocSection, index?: number) { const totalElements = source.getNumChildren(); for (let j = 0; j < totalElements; ++j) { if (index) copyElement(source.getChild(j), dest, index + j); else copyElement(source.getChild(j), dest); } } function spreadsheetRowToObject( sheet: GoogleAppsScript.Spreadsheet.Sheet, rownum: number ): { [key: string]: any } { // TODO: could be more efficient const values = sheet.getDataRange().getValues(); const headers = values[0]; const row_data = values[rownum - 1]; return headers.reduce((acc, header, index) => { acc[String(header)] = row_data[index]; return acc; }, {}); } function generateForCurrentRow() { const spreadsheet = SpreadsheetApp.getActive(); const cell = spreadsheet.getCurrentCell(); if (!cell) throw new Error('No Cell selected for operation on row'); const row = spreadsheetRowToObject( spreadsheet.getActiveSheet(), cell.getRow() ); const template_doc = DocumentApp.openById(row['Template ID']); const source_file = DriveApp.getFileById(row['Document ID']); const out_folder = DriveApp.getFolderById( '1ROyJXk-QANTHM6Jiw0ne3EQiR1f2UsLr' ); const out_file = source_file.makeCopy( row['Document Name'] + row['Version'], out_folder ); const out_doc = DocumentApp.openById(out_file.getId()); // Copy header out_doc.getHeader().clear(); copySection(template_doc.getHeader(), out_doc.getHeader()); // Copy footer out_doc.getFooter().clear(); copySection(template_doc.getFooter(), out_doc.getFooter()); const insert_point = template_doc .getBody() .findText('{{body}}') ?.getElement(); if (!insert_point) { throw new Error("Could not find insert point '{{body}}'"); } // find the parent element that is a direct descendant of the body let parent = insert_point; while (parent.getParent().getType() != DocumentApp.ElementType.BODY_SECTION) { parent = parent.getParent(); } const index = template_doc.getBody().getChildIndex(parent); // Copy body contents above and below {{body}} tag for (let j = 0; j < template_doc.getBody().getNumChildren(); j++) { if (j < index) copyElement(template_doc.getBody().getChild(j), out_doc.getBody(), j); // don't copy {{body}} tag else if (j == index) continue; else copyElement(template_doc.getBody().getChild(j), out_doc.getBody()); } // do text replacement Object.entries(row).forEach(([header, data]) => { out_doc.getBody().replaceText(`{{${header}}}`, String(data)); out_doc.getHeader().replaceText(`{{${header}}}`, String(data)); out_doc.getFooter().replaceText(`{{${header}}}`, String(data)); }); out_doc.saveAndClose(); // create PDF file out_folder.createFile(out_file.getAs('application/pdf')); }