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.