google-apps-scriptgoogle-docs

Copy text, images, tables, ALL formatting, margins from on GDoc to another


After trying a few mail merge scripts, I decided t write my own. My merge script runs as a separate fiIt reads a template from a GDoc, reads data from a GSpreadsheet, and merges it either into Gmails or into a new GDoc - one page / email per SS row.

The problem is that it doesn't copy text formatting, margins or images into the Gmail or new GDoc ... only the plain text.

I am using DocumentApp.openById > getActiveSection > getText() to capture the text.

Here is the code:

/**********************************************************************************/
/**********************************************************************************/
/**********************************************************************************/
/********                                                                  ********/
/********                   Document and Email Data Merge                  ********/
/********                                                                  ********/
/********                         ver 1.9.2013b                            ********/
/********                                                                  ********/
/**********************************************************************************/
/**********************************************************************************/
/**********************************************************************************/

function docAndEmailMerge() {

/**********************************************************************************/
/**********************************************************************************/
/********                         User Settings                            ********/
/**********************************************************************************/
/**********************************************************************************/

  // Identify the Google Spreadsheet document by its ID - data to be merged
  var SSID = "0AgJjDgtUl5KddFdQRHRnRjVmMVlvOWlRa3Z2MlFLdGc";
  
  // Identify the Google Document ID - document to merge
  var docID = "1fxcn9pW1XySfrhMQlFyi9Zvhf54cTz96N8Vdm0McZD4";

  // Settings
  var mergeToEmail = true;
  var mergeToSameDoc = true;
  var mergeToIndividualDoc = false; // Not working yet
     
/**********************************************************************************/
/**********************************************************************************/
/********                         Main Program                             ********/
/**********************************************************************************/
/**********************************************************************************/

   if (mergeToSameDoc == true) {
      var mergedDoc = DocumentApp.create('Data Merge');
      }  // end if
  
  // Sheet within SS that has the data to be proessed
  var sheet = "sheet1";
  
  // Row 1 = labels, row 2 = first row with good data
  var startRow = 2; 
    
  // Create spreadsheetApp object, open Spreadsheet
  var ss = SpreadsheetApp.openById(SSID);
    
  // Create sheet object, open active sheet within spreadsheet
  var activeSheet = ss.getSheetByName(sheet);
  
  // Grab the data from the spreadsheet, put into array object
  var array = ss.getDataRange().getValues();
  
  // Get number of rows and columns 
  numRows = array.length - 1;
  var numCol = activeSheet.getLastColumn();
  
  // Set the data range of the active spreadsheet
  var dataRange = activeSheet.getRange(1, 1, 1, numCol);
  
  // Get the first row labels into an array
  var firstRow = getFirstRowLabels(dataRange); 
  
  var dataRange = activeSheet.getRange(startRow, 1, numRows, numCol)
  
  // Fetch values for each row in the Range and put in multidimensional array
  var data = dataRange.getValues();
  
  // Working with the merge document
  var doc = DocumentApp.openById(docID);
  var body = doc.getActiveSection();
  var text = body.getText();     
  
  // Get text from body of document that is to be merged
  var text = body.getText();
  
  // This is for logger output
  var loggerOut = "";
    
  // Repeat this loop for each row in "data" array
  for (i in data) {
    var row = data[i];
     var temp = text;    
     for (var j = 2; j < numCol; j++) {
       var search = "%" + firstRow[j] + "%";            
       var temp = replaceAll(temp, search, row[j]);
     
     } // end for j
     loggerOut += temp;
    
    // Write results to Gmail or Docs
    writeData(mergeToEmail, mergeToSameDoc, row, mergedDoc, temp);
       
  } // end for i
 
  Logger.log(loggerOut);
  
} // End main program docAndEmailMerge

/**********************************************************************************/
/**********************************************************************************/
/********                         Sub Programs                             ********/
/**********************************************************************************/
/**********************************************************************************/

// Replace all occurances in a string
function replaceAll(Source,stringToFind,stringToReplace){
  var temp = Source;
    var index = temp.indexOf(stringToFind);
        while(index != -1){
            temp = temp.replace(stringToFind,stringToReplace);
            index = temp.indexOf(stringToFind);
        }
        return temp;
} // end functio replaceAll

// Get first row labels of a ss
function getFirstRowLabels(dataRange) {
  // Fetch values for each row in the Range and put in multidimensional array
  var data = dataRange.getValues();
 
  // Repeat this loop for each row in "data" array
  for (i in data) {
    var firstRow = data[i];
    } // end for
  return firstRow;
} // end getFirstRowLabels

function writeData(mergeToEmail, mergeToSameDoc, row, mergedDoc, temp) {
    
    if (mergeToEmail == true) {
      MailApp.sendEmail(row[0], row[1], temp);
      } // end if
    
    if (mergeToSameDoc == true) {
      mergedDoc.appendParagraph(temp);
      mergedDoc.appendPageBreak();      
      }  // end if
}


Solution

  • You should copy the template first using DocsList so you start with a "complete" initial document.

      var template = DocsList.getFileById(docIDs[0]);// get the template model, in this sample I had an array of possible templates, I took the first one
      var newmodelName=template.substr(0,11)+'multipage'+template.substring(18);// define a new name, do what you need here...
      var baseDocId = DocsList.copy(template,newmodelName).getId();// make a copy of firstelement and give it new basedocname build from the serie(to keep margins etc...)
      var baseDoc = DocumentApp.openById(baseDocId);// this is the new doc to modify
    

    then use the document class that has a direct replaceText method


    EDIT : about your secondary question, here is a suggestion on how you could do. It works nicely except for inlineImage, I'll keep looking at this. You could also make the script more universal by adding other element types...

    function myFunction() {
      var template = DocsList.getFileById(key);// get the template model
      var newmodelName='testcopy';// define a new name, do what you need here...
      var baseDocId = DocsList.copy(template,newmodelName).getId();// make a copy of firstelement and give it new basedocname build from the serie(to keep margins etc...)
      var baseDoc = DocumentApp.openById(baseDocId);// this is the new doc to modify
      var body = baseDoc.getActiveSection();
      body.appendPageBreak();
      var totalElements = body.getNumChildren();
      for( var j = 0; j < totalElements; ++j ) {
        var element = body.getChild(j).copy();
        var type = element.getType();
        if( type == DocumentApp.ElementType.PARAGRAPH )
          body.appendParagraph(element);
        else if( type == DocumentApp.ElementType.TABLE )
          body.appendTable(element);
        else if( type == DocumentApp.ElementType.LIST_ITEM )
          body.appendListItem(element);
        else if( type == DocumentApp.ElementType.INLINE_IMAGE )
          { var blob = body.getChild(j).asInlineImage().getBlob();
           body.appendImage(blob); }
      }
    }
    

    Edit 2 Thanks to @Fausto, here is a fully working version. Inline images are included in a paragraph so we had to dig one level more to get the blob...

    function myFunction() {
      var template = DocsList.getFileById(key);// get the template model
      var newmodelName='testcopy';// define a new name, do what you need here...
      var baseDocId = DocsList.copy(template,newmodelName).getId();// make a copy of firstelement and give it new basedocname build from the serie(to keep margins etc...)
      var baseDoc = DocumentApp.openById(baseDocId);// this is the new doc to modify
      var body = baseDoc.getActiveSection();
      body.appendPageBreak();
      var totalElements = body.getNumChildren();
      for( var j = 0; j < totalElements; ++j ) {
        var element = body.getChild(j).copy();
        var type = element.getType();
        if (type == DocumentApp.ElementType.PARAGRAPH) {
          if (element.asParagraph().getNumChildren() != 0 && element.asParagraph().getChild(0).getType() == DocumentApp.ElementType.INLINE_IMAGE) {
            var blob = element.asParagraph().getChild(0).asInlineImage().getBlob();
            body.appendImage(blob);
          }
          else body.appendParagraph(element.asParagraph());
        }
        else if( type == DocumentApp.ElementType.TABLE )
          body.appendTable(element);
        else if( type == DocumentApp.ElementType.LIST_ITEM )
          body.appendListItem(element);
      }
    }