Replace text in Google Doc table for form merge - google-apps-script

I am trying to make automated reports by filling a form. The data is read out by the script from the last spreadsheet line. Then the report is made as a Google Doc. Here some tags inside this document present where the items should be. %meterx% is for meters.
These can be images or text. For normal paragraphs, this is working fine with the first loop where the type = paragraph. But it skips tables. I need to replace the %meterx% in a table cell with the image, just like I do with the paragraph, but I am stuck at the code to look through the table.
I see some ways to replace text but this seems to be the only way to replace it with images.
var totalElements = doc.getNumChildren();
var el=[]
for( var j = 0; j < totalElements; ++j ) {
var element = doc.getChild(j);
var type = element.getType();
if (type =='PARAGRAPH'){
el[j]=element.getText()
if(el[j]=='%meter3%'){element.removeFromParent();
var newimage = UrlFetchApp.fetch('http://chart.googleapis.com/chart?chf=bg,s,67676700&chs=280x150&cht=gm&chds=0,10&chd=t:'+row[4]+'&chdlp=b').getBlob();
doc.insertImage(j, newimage);
if (type =='TABLE'){
var tablerows=element.getNumRows();
Logger.log(tablerows);
for ( var i = 0; i < tablerows; ++i ) {
var tablerow = element.getRow(0)
Logger.log(tablerow); // <--- gives TableRow
} /// STUCK !! :)

A TableRow contains child elements, of type TableCell, which also contain child elements. Those are type PARAGRAPH if they are blank or contain text.
You can access the text within a TableCell with getText(), but it's a good idea to confirm that the cell contains text first.
The code below handles PARAGRAPH and TABLE element types, and for tables it explores the TABLECELL elements. I don't know what you're doing with your array el[], so I've left that out, and also commented out the image-replacement code - instead, I'm just logging the structure and content of the table for illustration. To complete your goal, you should replace the logging with the same match & replace behavior you have for PARAGRAPH.
NOTE: I am using a couple of helper functions not detailed here, which should be self-explanatory, getFileByName_() and elementTypeToText_().
function Q13869576() {
var folder = "StackOverflow";
var docname = "Q13869576.gdoc";
var docId = getFileByName_(folder, docname).getId();
var doc = DocumentApp.openById(docId);
var docBody = doc.getActiveSection();
var totalElements = doc.getNumChildren();
var el=[]
for( var j = 0; j < totalElements; ++j ) {
var element = doc.getChild(j);
var type = element.getType();
switch (type) {
case DocumentApp.ElementType.PARAGRAPH:
el[j]=element.getText()
if(el[j]=='%meter3%'){
Logger.log( "Found tag in paragraph" );
// element.removeFromParent();
// var newimage = UrlFetchApp.fetch('http://chart.googleapis.com/chart?chf=bg,s,67676700&chs=280x150&cht=gm&chds=0,10&chd=t:'+row[4]+'&chdlp=b').getBlob();
// doc.insertImage(j, newimage);
}
break;
case DocumentApp.ElementType.TABLE:
var tablerows=element.getNumRows();
Logger.log(tablerows);
for ( var row = 0; row < tablerows; ++row ) {
var tablerow = element.getRow(row)
for ( var cell=0; cell < tablerow.getNumCells(); ++cell) {
Logger.log( "Table Row("+row+") Cell ("+cell+")");
Logger.log(
elementTypeToText_(tablerow.getChild(cell).getType())
+" with child type "
+elementTypeToText_(tablerow.getChild(cell).getChild(0).getType()));
var celltext = tablerow.getChild(cell).getText();
Logger.log( "Text is ("+celltext+")" );
}
}
break;
}
}
}
This is an excerpt of the logs from a sample doc, with a table containing a cell with the %meter3% tag:
...
Table Row(1) Cell (2)
TABLE_CELL with child type PARAGRAPH
Text is (%meter3%)
...

Related

Google Appscript copy and paste google docs content without space in the header space

I have followed this script How to copy content and formatting between Google Docs? to copy the content from one google doc and paste it to another, which works great, however everytime the content is pasted, there is a space on top of the pasted content, see below. How can the content be pasted properly?
Source file: https://docs.google.com/document/d/1xVpJM4hSN3fosFXR16JbZ1_7r0_PxV92T-G24X5LQRo/edit
Target file: https://docs.google.com/document/d/1g9oon4e0FDBF2fbexVCR-uxKko3B6-Hpj850kiH3qXo/edit
Basically the table from the source file will get copied and pasted to the target file for multiple times, and the tables should sit side by side on the target file without space on top which breaks the format.
appscript is inbedded in the source file
function copyDoc() {
var sourceDoc = DocumentApp.getActiveDocument().getBody();
// var targetDoc = DocumentApp.create('CopyOf'+DocumentApp.getActiveDocument().getName());
var targetDoc = DocumentApp.openById('1g9oon4e0FDBF2fbexVCR-uxKko3B6-Hpj850kiH3qXo');
var totalElements = sourceDoc.getNumChildren();
for( var j = 0; j < totalElements; ++j ) {
var body = targetDoc.getBody()
var element = sourceDoc.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);
}
// ...add other conditions (headers, footers...
}
targetDoc.saveAndClose();
}
Edit1: Based on TheWizEd's answer, here's how it looks like, format looks very off.
Why not copy the table cell into a new cell to the right. Try this.
First I copy the Source or template to a new file. Then I find the table and loop through each row duplicating the cell from column 1 to new column 2. If you wanted to add another row you could duplicate the first row as many times as you want to the new table.
However I notice you are using replacable text. You will have to have a different script to replace the text in column 1 vs. column 2.
function copyDoc() {
try {
let source = DocumentApp.getActiveDocument();
let file = DriveApp.getFileById(source.getId()).makeCopy("Target");
let target = DocumentApp.openById(file.getId());
let body = target.getBody();
for( let i=0; i<body.getNumChildren(); i++ ) {
let child = body.getChild(i);
if( child.getType() === DocumentApp.ElementType.TABLE ) {
child = child.asTable();
for( let i=0; i<child.getNumRows(); i++ ) {
let sRow = child.getRow(i)
let tRow = sRow.copy();
for( let j=0; j<tRow.getNumCells(); j++ ) {
let cell = tRow.getCell(j).copy();
sRow.appendTableCell(cell);
}
}
}
}
}
catch(err){
console.log(err);
}
}
Here is the result of my script.

setAttributes in Google Doc header failing - setAttibutes is not a function

I want to set the font size in a google doc header, but I get the error 'setAttributes is not a function'. I can't really understand the structure of a header (it seems), as to where to set the attributes.
var style = {};
style[DocumentApp.Attribute.FONT_SIZE] = 16;
style[DocumentApp.Attribute.BOLD] = true;
var range = this.Doc.getHeader(
range.setText(text)
var m = range.getNumChildren();
for (var i =0; i<m;i++){
var cld = range.getChild(i);
var ct = cld.getText();
var cat = cld.getAttributes();
cld.setAttibutes(style);
}
In the code above I can set the text in the header, and I can see the text in the 1st child element "ct", but I can't set the attributes. cld.getAttributes() returns nulls, so I'm thinking the attributes are set on a higher element. I just don't know which.
Issue:
getChild returns element, not Text, thus you need to change cld to Text via asText and then use getText().
As for the setAttributes function, I didn't encounter the issue you had, it just worked so I have no clue on why it errors on yours.
Can you try this one?
Code:
function updateHeader() {
var doc = DocumentApp.getActiveDocument();
var range = doc.getHeader();
var text = "this is the new header";
var style = {};
style[DocumentApp.Attribute.FONT_SIZE] = 16;
style[DocumentApp.Attribute.BOLD] = true;
var m = range.getNumChildren();
for (var i = 0; i < m; i++){
var cld = range.getChild(i);
var ct = cld.asText().getText(); // get text value
var cat = cld.getAttributes(); // get attributes
Logger.log(ct); // print old text
Logger.log(cat); // print old attributes
cld.asText().setText(text); // set text as header value
cld.setAttributes(style); // set attributes
Logger.log(cld.asText().getText()); // print new text
Logger.log(cld.getAttributes()); // print new attributes
}
}
Sample:
Output:
Logs:
Note:
I placed setText inside the loop to show you the difference of before and after the update of the value of the text and attribute.

How to I copy one google doc into another

I am wondering how to copy a google doc, but include the style, links and font, from within google scripts.
I have tried to use the getBody()
DocumentApp.getActiveDocument().getBody().clear()
document2 = DocumentApp.openById("Document_ID").getBody().getText()
DocumentApp.getActiveDocument().getBody().appendParagraph(document2)
But that only copies that raw text.
EDIT:
Removed documents because the question was solved
Your problem is that getText will only return the raw text, nothing more. Treat them as objects instead, iterate, then append one by one.
function appendContents() {
var source = DocumentApp.getActiveDocument().getBody();
var destination = DocumentApp.openById("DOC ID");
var numElements = source.getNumChildren();
for (var i = 0; i < numElements; ++i ) {
var body = destination.getBody()
var element = source.getChild(i).copy();
var type = element.getType();
if( type == DocumentApp.ElementType.PARAGRAPH ){
body.appendParagraph(element);
}
// Add other element types if you are expecting other elements
// e.g. LIST_ITEM, TEXT, TABLE
// Note that different append will be used for each element type.
}
destination.saveAndClose();
}
If you want to add other element types, see list of available ElementTypes

Get correct user-selected text in Google Docs Apps Script

I need the full text of a user-made selection in Google Docs which may stretch over multiple lines.
This is different from this question: Get user-selected text
because in that question the request was to get the text of individual words or lines that are selected. I need to be able to handle multiple lines with the correct whitespace.
I wrote the code below, and it almost works, but it smashes the text from two lines together without a newline character (or anything) separating them. It also doesn't include tabs from the original text. I'm guessing there are other limitations I haven't come across!
function getSelectedText()
{
var selection = DocumentApp.getActiveDocument().getSelection();
if( !selection )
return "";
var selectedElements = selection.getRangeElements();
var theText = "";
for( var i=0; i < selectedElements.length; i++ )
{
var thisText = selectedElements[i].getElement().asText().getText();
if( selectedElements[i].isPartial() )
{
var thisText = thisText.substring( selectedElements[i].getStartOffset(), selectedElements[i].getEndOffsetInclusive() + 1)
}
theText += thisText;
}
return theText;
}
So let's say I have a document that looks like this with all the text selected by the user (with a tab in the second line):
Line 1
Line 2
My script will construct the string, "Line 1Line 2".
The string I would like is, "Line 1\nLine 2" or "Line 1\rLine 2".
This function came from a function I wrote to high light selected text. I modified it to append selected text back into the document so that it could be displayed in a non html format which will not display white space. Hopefully this will be helpful to you.
function getCurrentSelection() {
var doc=DocumentApp.getActiveDocument();
var selection=doc.getSelection();
var total="";
if(selection) {
var selectedElements = selection.getRangeElements();
for(var i=0;i<selectedElements.length;i++) {
var selElem = selectedElements[i];
var el = selElem.getElement();
var isPartial = selElem.isPartial();
if(isPartial) {
var selStart = selElem.getStartOffset();
var selEnd = selElem.getEndOffsetInclusive();
}else {
var selStart = selElem.getStartOffset();
var selEnd = selElem.getEndOffsetInclusive();
}
var elType=el.getType();
if(elType==DocumentApp.ElementType.TEXT) {
var txt = selElem.getElement().asText().getText().slice(selStart,selEnd+1);
}
if(elType==DocumentApp.ElementType.PARAGRAPH) {
var txt = selElem.getElement().asParagraph().getText();
}
total+=txt;
}
}else {
total='';
}
doc.getBody().appendParagraph(total);
}
I have managed to copy the selected text (with line-breaks and tabs) with the following modification of your code:
function myFunction() {
var doc=DocumentApp.getActiveDocument();
var selection = DocumentApp.getActiveDocument().getSelection();
if( !selection )
return "";
var selectedElements = selection.getRangeElements();
for( var i=0; i < selectedElements.length; i++ )
{
var thisText = selectedElements[i].getElement().asText().getText();
doc.getBody().appendParagraph(thisText);
}
}
On selectedElements you get each of the paragraphs you have selected, lets say 2 paragraphs and an intro in the middle. Then you append each of these elements / paragraphs as a new paragraph.
Check if this works for you and let me know!
Kessy's answer made me hypothesize I'm overthinking what elements are, and that it may be appropriate to manually add a single carriage return between them in constructing a string. Here's my original code with that slight modification.
So far it works, but if my assumption about elements is wrong, it may fail with text formatted in some untested way. (Though if text is formatted in a much more complicated way, it may not make sense to have a specific and unique string version of it anyway. E.g. text in a table.)
function getSelectedText()
{
var selection = DocumentApp.getActiveDocument().getSelection();
if( !selection )
return "";
var selectedElements = selection.getRangeElements();
var theText = "";
for( var i=0; i < selectedElements.length; i++ )
{
var thisText = selectedElements[i].getElement().asText().getText();
if( selectedElements[i].isPartial() )
var thisText = thisText.substring( selectedElements[i].getStartOffset(), selectedElements[i].getEndOffsetInclusive() + 1)
theText += thisText;
//I'm assuming each element is separated by one carriage return.
if( i+1 < selectedElements.length )
theText += '\r';
}
return theText;
}

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 in GDoc http://goo.gl/fO5vP
I can't seem to share a script so I had to put it in a doc. Copy it to a new script and it will be color coded.
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);
}
}