2

I am trying to copy formatted text from one document and paste it into another. I want to take the entirety of one document and add it to another via Google App Script.

The call body.getText() satisfies my use case but gets the text as a string, not formatted.

It would be great to be able to copy formatted text from one document to another.

EDIT: Taking the advice I currently wrote some more code. Following the other answer almost exactly, I am still only getting the text and not the formatting too.

  for(var i = 0; i < numElements; ++i) {
  var element = copyBody.getChild(i)
  var type = element.getType();
   if (type == DocumentApp.ElementType.PARAGRAPH)
   {
     var newElement = element.copy().asParagraph();
     newBody.appendParagraph(newElement); 
   }
   else if(type == DocumentApp.ElementType.TABLE)
   {
     var newElement = element.copy().asTable();
     newBody.appendTable(newElement); 
   }
   else if(type == DocumentApp.ElementType.LIST_ITEM)
   {     
     var newElement = element.copy().asListItem();
     newBody.appendListItem(newElement);
   }
    else{
    Logger.log("WRONG ELEMENT")    
    }
  }    
Alex
  • 135
  • 2
  • 13
  • "Almost" - Henrique's function doesn't resort to using `.asParagraph()`, etc. Try with plain `.copy()`. The script in my answer works as-is. – Mogsdad Jul 10 '13 at 19:38

2 Answers2

6

This answer covers it.

You need to iterate through the elements of the source document, appending each to the destination doc. You don't copy Text versions of paragraphs, etc., instead you copy the whole element including formatting etc.

Script

Since Documents now support programmable UI elements, here's a script based on Henrique's previous answer (above), that includes a Custom Menu to drive the merge of documents. You may optionally include page breaks between appended documents - useful if you are trying to create a multi-chapter document.

This script must be contained in a Document (or no UI for you)!

/**
 * The onOpen function runs automatically when the Google Docs document is
 * opened. 
 */
function onOpen() {
  DocumentApp.getUi().createMenu('Custom Menu')
      .addItem('Append Document','appendDoc')
      .addToUi();
}

/**
 * Shows a custom HTML user interface in a dialog above the Google Docs editor.
 */
function appendDoc() {
  // HTML for form is rendered inline here.
  var html =
      '<script>'
  +     'function showOutput(message) {'
  +       'var div = document.getElementById("output");'
  +       'div.innerHTML = message;'
  +     '}'
  +   '</script>'
  +   '<form id="appendDoc">'
  +     'Source Document ID: <input type="text" size=60 name="docID"><br>'
  +     'Insert Page Break: <input type="checkbox" name="pagebreak" value="pagebreak">'
  +     '<input type="button" value="Begin" '
  +       'onclick="google.script.run.withSuccessHandler(showOutput).processAppendDocForm(this.parentNode)" />'
  +   '</form>' 
  +   '<br>'
  +   '<div id="output"></div>'

  DocumentApp.getUi().showDialog(
    HtmlService.createHtmlOutput(html)
               .setTitle('Append Document')
               .setWidth(400 /* pixels */)
               .setHeight(150 /* pixels */));
}

/**
 * Handler called when appendDoc form submitted.
 */
function processAppendDocForm(formObject) {
  Logger.log(JSON.stringify(formObject));
  var pagebreak = (formObject.pagebreak == 'pagebreak');
  mergeDocs([DocumentApp.getActiveDocument().getId(),formObject.docID],pagebreak);
  return "Document appended.";
}

/**
 * Updates first document in list by appending all others.
 *
 * Modified version of Henrique's mergeDocs().
 * https://stackoverflow.com/a/10833393/1677912
 *
 * @param {Array} docIDs      Array of documents to merge.
 * @param {Boolean} pagebreak Set true if a page break is desired
 *                              between appended documents.
 */
function mergeDocs(docIDs,pagebreak) {
  var baseDoc = DocumentApp.openById(docIDs[0]);
  var body = baseDoc.getBody();

  for( var i = 1; i < docIDs.length; ++i ) {
    if (pagebreak) body.appendPageBreak();
    var otherBody = DocumentApp.openById(docIDs[i]).getBody(); 
    Logger.log(otherBody.getAttributes());
    var totalElements = otherBody.getNumChildren();
    var latestElement;
    for( var j = 0; j < totalElements; ++j ) {
      var element = otherBody.getChild(j).copy();
      var attributes = otherBody.getChild(j).getAttributes();
      // Log attributes for comparison
      Logger.log(attributes);
      Logger.log(element.getAttributes());
      var type = element.getType(); 
      if (type == DocumentApp.ElementType.PARAGRAPH) {
        if (element.asParagraph().getNumChildren() != 0 && element.asParagraph().getChild(0).getType() == DocumentApp.ElementType.INLINE_IMAGE) {
          var pictattr = element.asParagraph().getChild(0).asInlineImage().getAttributes();
          var blob = element.asParagraph().getChild(0).asInlineImage().getBlob();
          // Image attributes, e.g. size, do not survive the copy, and need to be applied separately
          latestElement = body.appendImage(blob);
          latestElement.setAttributes(clean(pictattr));
        }
        else latestElement = body.appendParagraph(element);
      }
      else if( type == DocumentApp.ElementType.TABLE )
        latestElement = body.appendTable(element);
      else if( type == DocumentApp.ElementType.LIST_ITEM )
        latestElement = body.appendListItem(element);
      else
        throw new Error("Unsupported element type: "+type);
      // If you find that element attributes are not coming through, uncomment the following
      // line to explicitly copy the element attributes from the original doc.
      //latestElement.setAttributes(clean(attributes));
    }
  }
}


/**
 * Remove null attributes in style object, obtained by call to
 * .getAttributes().
 * https://code.google.com/p/google-apps-script-issues/issues/detail?id=2899
 */
function clean(style) {
  for (var attr in style) {
    if (style[attr] == null) delete style[attr];
  }
  return style;
}

Edit: Adopted handling of inline images from Serge's answer, with handling of image size attributes. As the comments indicate, there have been problems with some attributes being snarfed in the append, thus the introduction of the clean() helper function and use of .setAttributes(). However, you'll note that the call to .setAttributes() is commented out; that's because it too has a side-effect that will remove some formatting. It's your choice about which annoyance you'd rather deal with.

Community
  • 1
  • 1
Mogsdad
  • 44,709
  • 21
  • 151
  • 275
  • Hi Mogsdad, I tested your code out of curiosity mainly because I liked the html construction from within the script but I get an error showing up in the popup panel : Google Drive encountered an error. If reloading the page doesn't help, please report the error. Is it a momentary failure ? does it work for you ? – Serge insas Jul 10 '13 at 19:38
  • The script service just burped... I saw the same for a few minutes. (Did you authorize the script first?) – Mogsdad Jul 10 '13 at 19:39
  • I am in the same boat for the service. I thought getActiveSection() was a deprecated method. – Alex Jul 10 '13 at 19:41
  • Yeah, but it still works. I'd changed the `getActiveSection()` to `.getBody()` for the "baseDoc" so I could get autocomplete to work further on while I played, but missed the second instance. I've updated it here, but can't touch the real script to test currently since the service is down. – Mogsdad Jul 10 '13 at 19:47
  • I copied and pasted your code verbatim and currently the formatting is not coming over exactly. For example, the header is not in the same font. The bulleted list is also not bulleted just indented. – Alex Jul 10 '13 at 21:04
  • You're right - not everything survives. I've got a bullet list that ends up with numbers. And the second time the same doc is appended the numbering continues rather than starting from 1. Various font changes are all making it, including Header 1 with a non-standard font. It might have to do with what element the format changes are applied to, e.g. specific text within a paragraph vs the whole paragraph. (Maybe a deeper copy of elements is needed?) – Mogsdad Jul 10 '13 at 21:19
  • I added the possibility to get inlineImages copied as well, I'll post the relevant part of code in a new answer for comfort. – Serge insas Jul 10 '13 at 21:21
  • @Mogsdad The documentation says that it returns a "deep copy" [link](https://developers.google.com/apps-script/reference/document/body#copy()) – Alex Jul 10 '13 at 21:32
  • The deep copy seems true, but there's a problem when that copy is appended. One google bug found: I've compared the attributes of the "list items", and they match before appending the new element; `GLYPH_TYPE=BULLET`. AFTER appending, the attributes on the new element CHANGED, `GLYPH_TYPE=null`, same happens with `GLYPH_TYPE:NUMBER`. – Mogsdad Jul 10 '13 at 21:49
  • I tried appending the attributes hastable that comes back from the `getAttributes()` call but they are not in correct format to set them in the `body.appendTable()` call. – Alex Jul 10 '13 at 22:03
  • That's [Issue 2899](https://code.google.com/p/google-apps-script-issues/issues/detail?id=2899). Workaround is to remove the null attributes, let's see if that works. – Mogsdad Jul 11 '13 at 01:50
1

here is a slight improvement to Mogsdad script to get inline images copied as well.

The only issue is that if an image has been resized it is not keeping this new size in the copy, images are shown in their original size... No idea right now how to solve that .

function mergeDocs(docIDs,pagebreak) {
  var baseDoc = DocumentApp.openById(docIDs[0]);
  var body = baseDoc.getBody();

  for( var i = 1; i < docIDs.length; ++i ) {
    if (pagebreak) body.appendPageBreak();
    var otherBody = DocumentApp.openById(docIDs[i]).getBody();
    var totalElements = otherBody.getNumChildren();
    for( var j = 0; j < totalElements; ++j ) {
      var element = otherBody.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);
      else
        throw new Error("According to the doc this type couldn't appear in the body: "+type);
    }
  }
}

you can test it on this doc ID : 1E6yoROb52QjICsEbGVXIBdz8KhdFU_5gimWlJUbu8DI

(Execution succeeded [43.896 seconds total runtime]) !!! be patient !

this code comes from this other post

Community
  • 1
  • 1
Serge insas
  • 45,904
  • 7
  • 105
  • 131
  • Serge, I figured out the image resize problem, and updated the code in my answer. – Mogsdad Jul 11 '13 at 02:51
  • Now... maybe you can fix the form handling in mine, so it gives progress feedback! – Mogsdad Jul 11 '13 at 03:00
  • I'm not comfortable enough with html service, in UiApp I'd do it in seconds with a clientHandler and an animated gif but I still have a lot to learn with this new 'way of thinking' I can read your code but I'm not yet able to write it ;-) (one good point : the opposite would be even worse, right ? ^^) – Serge insas Jul 11 '13 at 21:44