const DOCUMENT_TEMPLATE_IDS = { Certification: '1V0uMuM80BGpjpdt1AmuzlU97tDI_u-y2rOfdl4tkqmc', }; const OUTPUT_FOLDER_ID = '1ROyJXk-QANTHM6Jiw0ne3EQiR1f2UsLr'; declare namespace GoogleAppsScript { namespace Spreadsheet { interface RichTextValue { getLinkUrl(): string | null; getLinkUrl(startOffset: number, endOffset: number): string | null; } } } type DocSection = | GoogleAppsScript.Document.Body | GoogleAppsScript.Document.HeaderSection | GoogleAppsScript.Document.FooterSection; function onOpen() { const ui = SpreadsheetApp.getUi(); ui.createMenu('CMS Document Generation') .addItem('Generate document for current row', 'generateForCurrentRow') .addItem('Generate document for all rows', 'generateForAllRows') // .addItem('CAUTION! Write list of all files to current sheet', 'dumpFiles') .addToUi(); } function findAllFiles(path: string, folder: GoogleAppsScript.Drive.Folder) { const out: string[][] = []; const files = folder.getFiles(); while (files.hasNext()) { const file = files.next(); if (!file.isTrashed()) { out.push([ path, file.getId(), file.getName(), `=HYPERLINK("${file.getUrl()}", "${file.getName()}")`, ]); } } const subfolders = folder.getFolders(); while (subfolders.hasNext()) { const subfolder = subfolders.next(); out.push(...findAllFiles(path + '/' + subfolder.getName(), subfolder)); } return out; } function dumpFiles() { const root = DriveApp.getFolderById('1mWOQDmQjQsi8the09VXpnKnvFUzj4wiv'); const out = findAllFiles('', root); const active_sheet = SpreadsheetApp.getActive(); const range = active_sheet.getRange(`R1C1:R${out.length}C${out[0].length}`); range.setValues(out); } function copyElement( source_element: GoogleAppsScript.Document.Element, dest: DocSection, index?: number ) { const element = source_element.copy(); switch (element.getType()) { case DocumentApp.ElementType.PARAGRAPH: if (index !== undefined) dest.insertParagraph(index, element.asParagraph()); else dest.appendParagraph(element.asParagraph()); break; case DocumentApp.ElementType.TABLE: if (index !== undefined) dest.insertTable(index, element.asTable()); else dest.appendTable(element.asTable()); break; case DocumentApp.ElementType.LIST_ITEM: if (index !== undefined) dest.insertListItem(index, element.asListItem()); else dest.appendListItem(element.asListItem()); break; case DocumentApp.ElementType.INLINE_IMAGE: if (index !== undefined) dest.insertImage(index, element.asInlineImage()); else dest.appendImage(element.asInlineImage()); break; default: 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]: GoogleAppsScript.Spreadsheet.RichTextValue } { // TODO: could be more efficient const range = sheet.getDataRange(); const headers = range.getValues()[0]; const row_data = range.getRichTextValues()[rownum - 1]; return headers.reduce((acc, header, index) => { acc[String(header)] = row_data[index]; return acc; }, {}); } function trashFiles(files: GoogleAppsScript.Drive.FileIterator) { while (files.hasNext()) { files.next().setTrashed(true); } } function generateForRow( spreadsheet: GoogleAppsScript.Spreadsheet.Spreadsheet, row_num: number ) { const row = spreadsheetRowToObject(spreadsheet.getActiveSheet(), row_num); if (!(row['Type'].getText() in DOCUMENT_TEMPLATE_IDS)) throw new Error(`${row['Type']} is not a valid type of document!`); const template_doc = DocumentApp.openById( DOCUMENT_TEMPLATE_IDS[ row['Type'].getText() as keyof typeof DOCUMENT_TEMPLATE_IDS ] ); const link = row['Document'].getLinkUrl(); if (link === null) { throw new Error(`Link missing for ${row['Document'].getText()}`); } // TODO: should probably handle this better if (!link.includes('document')) return; const source_file = DriveApp.getFileById(DocumentApp.openByUrl(link).getId()); const out_folder = DriveApp.getFolderById(OUTPUT_FOLDER_ID); const out_name = row['Document'] + '_' + row['Version']; // Delete old files with the same name trashFiles(out_folder.getFilesByName(out_name)); trashFiles(out_folder.getFilesByName(out_name + '.pdf')); // Duplicate source document and open const out_file = source_file.makeCopy(out_name, out_folder); const out_doc = DocumentApp.openById(out_file.getId()); // Copy header if (!out_doc.getHeader()) out_doc.addHeader(); copySection(template_doc.getHeader(), out_doc.getHeader().clear()); // Copy footer if (!out_doc.getFooter()) out_doc.addFooter(); copySection(template_doc.getFooter(), out_doc.getFooter().clear()); 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]) => { let replacement = data.getText(); out_doc.getBody().replaceText(`{{${header}}}`, replacement); out_doc.getHeader().replaceText(`{{${header}}}`, replacement); out_doc.getFooter().replaceText(`{{${header}}}`, replacement); }); out_doc.saveAndClose(); // create PDF file out_folder.createFile(out_file.getAs('application/pdf')); } function generateForCurrentRow() { const spreadsheet = SpreadsheetApp.getActive(); const cell = spreadsheet.getCurrentCell(); if (!cell) throw new Error('No Cell selected for operation on row'); generateForRow(spreadsheet, cell.getRow()); } function generateForAllRows() { const spreadsheet = SpreadsheetApp.getActive(); const row_count = spreadsheet.getActiveSheet().getDataRange().getNumRows(); for (let row_num = 2; row_num < row_count; row_num++) { generateForRow(spreadsheet, row_num); } }