Converting formatted Text from Google sheets to Google docs - google-apps-script

I am trying to copy formatted texts from google sheets to google docs using google scripts. I have successfully converted text from sheets to docs however I am unable to carry over the relevant formatting/markdowns like bold, italics, colour, underlined & etc. Does anyone have any idea as to what I am doing wrong or what functions I can use in the google scripting library which allows me to copy over the formatting as well?
Currently, I have an existing google doc that acts as the template. All future google docs created will follow a similar template. I have created a sheet named 'doc Builder' and have used ,for loops and switch statements to choose which cell within the sheet to be copied over to the word doc.
function createDocument() {
var docbuilder = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('doc Builder');
//active data range
var range = docbuilder.getRange(4, 1, docbuilder.getLastRow() - 3, docbuilder.getLastColumn() - 1).getDisplayValues();
var templateId = 'myworddocIDwhichihaveremoved'; //the word doc
//Make a copy of the template file
var documentId = DriveApp.getFileById(templateId).makeCopy().getId();
//Rename the copied file
DriveApp.getFileById(documentId).setName('name of new doc');
//Get the document body as a variable
var body = DocumentApp.openById(documentId).getBody();
//copies texts from cell to word doc
//i = row, j = column
for(var i = 0; i < range.length; i++){
for(var j = 0; j < range[i].length; j++){
var cells = [];
switch(j) {
case 0:
body.appendParagraph(range[i][j]);
break;
case 1:
body.appendParagraph(range[i][j]);
break;
case 2:
if(range[i][j] != ""){
body.appendParagraph('PARAGRAPH 1:' + range[i][j]);
}
break;
case 3:
body.appendParagraph('PARAGRAPH 2:' + range[i][j]);
break;
}
}
}
}
I have tried copyTo() and it copies the formatting from sheet to sheet successfully however am unable to do the same for sheet to doc. I am also aware of the attributes which I can add to my word doc like BACKGROUND_COLOR, BOLD and etc from the documentation however the data I am handling often only has some parts of the cell formatted for example : sally is a girl instead of sally is a girl. Thus making it difficult to hard code when the number of cells increases.
Simply put I am trying to bring over the formatting from the sheet to the doc so I don't have to handle each cell individually.
I am working with more cases but I have removed them to simplify the code, also every cell within the active data range is formatted but when the new google doc is created the formatting disappears.
I hope someone has a solution to this haha :""D

Copying values from Sheets to Docs with formatting
There is no native method that you can use to copy formatted text from Sheets to Docs. They don't use the same classes to handle formatted text.
Sheets has RichTextValue that contains all the information for a cell. For example, when you call:
const range = sheet.getRange("A2")
const richTextValue = range.getRichTextValue()
You then can obtain all the information about the text formatting within the cell. For example, if you have a cell like this:
If you get the rich text value from this cell and then call the getRuns() method on this value you will get a series of new RichTextValue:
wherein each run is the longest possible substring having a consistent text style.
So for the example you will get a new object for:
"Hello"
"bold"
"italic"
... etc
You may also get individual object for the spaces between words.
For each of these objects, you can call a series of methods to get the individual components of its format:
getFontFamily()
getFontSize()
getForegroundColor()
isBold()
isItalic()
isStrikethrough()
isUnderline()
NOTE: getBackgroundColor() is not used in this example because background color in sheets cannot apply to single text runs, but the whole cell.
There is no equivalent class in DocumentApp. You can't append a RichTextValue to any element in a document. So this means that you need to match up the corresponding methods that you need. For example, you could use the Text class which has all the corresponding methods, you would just need a go-between to link up the methods and sequence them in the right way.
Example implementation
This would most likely need to be adapted to your exact needs, I don't know what the logic of the switch statements are and I don't have sample data to test it with, but this should give you a good idea of how it might work. You may also be able to use the custom class as-is in your script.
Ideally you would be able to call some simple methods from the main script, something like this:
function main() {
// Getting the rich text value
const sheet = SpreadsheetApp.getActive();.getSheetByName("Sheet1");
const range = sheet.getRange("A2");
const value = range.getRichTextValue();
// Creating an instance of a custom class that will be implemented
const textToExport = new SheetRichText(value)
// Setting the target document
const doc = DocumentApp.openById("[YOUR DOCUMENT ID]")
const body = doc.getBody()
// Calling a method of the custom class
textToExport.appendAsNewParagraph(body)
}
NOTE: Replace [YOUR DOCUMENT ID] with the correct document ID.
Remember that in my example my sheet has this:
The custom class I have implemented in my example is:
class SheetRichText{
// To initialize it you pass it a RichTextValue object
constructor(RichTextValue){
// It gets all the runs and creates an object that contains all the information
// needed to call the corresponding methods in the document Text class.
this.runs = RichTextValue.getRuns().map(run => {
const style = run.getTextStyle()
return {
"style" : {
"fontFamily" : style.getFontFamily(),
"fontSize" : style.getFontSize(),
"foregroundColor" : style.getForegroundColor(),
"bold" : style.isBold(),
"italic" : style.isItalic(),
"strikethrough" : style.isStrikethrough(),
"underline" : style.isUnderline()
},
"text" : run.getText(),
"start" : run.getStartIndex(),
"end" : run.getEndIndex()
}
})
}
// This takes as an argument the body of a document and adds the RichTextValue
// to the document as a new paragraph
appendAsNewParagraph(body){
// Initializing the new blank paragraph
const paragraph = body.appendParagraph("")
// For each run, copy the text and then set all the formatting
// making sure that the start and end indices are called.
this.runs.forEach(run => {
const textElement = paragraph.asText().appendText(run.text)
const [start, end] = [run.start, run.end -1]
textElement.setFontFamily(start, end, run.style.fontFamily)
textElement.setFontSize(start, end, run.style.fontSize)
textElement.setForegroundColor(start, end, run.style.foregroundColor)
textElement.setBold(start, end, run.style.bold)
textElement.setItalic(start, end, run.style.italic)
textElement.setStrikethrough(start, end, run.style.strikethrough)
textElement.setUnderline(start, end, run.style.underline)
})
}
}
Which results in:
References
Sheets RichTextValue
Docs Text

Related

How can I automatically insert images for each row of Google Sheets document to a Slides template (mail merge) using Apps Script?

I want that Apps Script to automatically generate a new set of slides using data from a Sheets document which has rows of the different information I want inserted into a Slides template replacing the placeholder tags. I want it to do it instantly for each row inside the table with one action, so if there are 10 rows, 10 sets of Slides documents will be generated.
The text replacement works, however I'm not sure how to replace, for example, a placeholder tag with "{{image}}"
The Image is a generated Qr code in column (N) with an sheet addon (QR Code for Classroom Attendance)and for each row separate. For example 10 rows with different qr codes. This addon writes the generate QR code in the column N. As I said for each I have a different Qr code.
function mailMergeSlidesFromSheets() {
// Load data from the spreadsheet
var dataRange = SpreadsheetApp.getActive().getDataRange();
var sheetContents = dataRange.getValues();
// Save the header in a variable called header
var header = sheetContents.shift();
// Create an array to save the data to be written back to the sheet.
// We'll use this array to save links to Google Slides.
var updatedContents = [];
// Add the header to the array that will be written back
// to the sheet.
updatedContents.push(header);
// For each row, see if the 4th column is empty.
// If it is empty, it means that a slide deck hasn't been
// created yet.
sheetContents.forEach(function(row) {
if(row[14] === "") {
// Create a Google Slides presentation using
// information from the row.
var slides = createSlidesFromRow(row);
var slidesId = slides.getId();
// Create the Google Slides' URL using its Id.
var slidesUrl = `https://docs.google.com/presentation/d/${slidesId}/edit`;
// Add this URL to the 4th column of the row and add this row
// to the updatedContents array to be written back to the sheet.
row[14] = slidesUrl;
updatedContents.push(row);
}
});
// Write the updated data back to the Google Sheets spreadsheet.
dataRange.setValues(updatedContents);
}
function createSlidesFromRow(row) {
// Create a copy of the Slides template
var deck = createCopyOfSlidesTemplate();
// Rename the deck using the firstname and lastname of the student
deck.setName(row[4] + " " + row[9] + row[3]);
// Replace template variables using the student's information.
deck.replaceAllText("{{id}}", row[0]);
deck.replaceAllText("{{tag}}", row[3]);
deck.replaceAllText("{{besetzung}}", row[4]);
deck.replaceAllText("{{beginn}}", row[5]);
deck.replaceAllText("{{ende}}", row[6]);
deck.replaceAllText("{{halle}}", row[7]);
deck.replaceAllText("{{stand}}", row[8]);
deck.replaceAllText("{{firma}}", row[2]);
deck.replaceAllText("{{veranstaltung}}", row[9]);
deck.insertImage("{{image}}", row[13]);
return deck;
}
function createCopyOfSlidesTemplate() {
//
var TEMPLATE_ID = "19PKvWoDtbeVHcqm4DnWUxRx1OBO817uG3cL5Ox-dQoo";
// Create a copy of the file using DriveApp
var copy = DriveApp.getFileById(TEMPLATE_ID).makeCopy();
// Load the copy using the SlidesApp.
var slides = SlidesApp.openById(copy.getId());
return slides;
}
function onOpen() {
// Create a custom menu to make it easy to run the Mail Merge
// script from the sheet.
SpreadsheetApp.getUi().createMenu("⚙️ Create BWN by Pavlos")
.addItem("Create Slides","mailMergeSlidesFromSheets")
.addToUi();
}
replaces the placeholder tags with the desired text
// I am not sure how to insert something similar for images and charts in the code here
// I've tried variations of the below, none of which have worked
// picture.find("{{image}}").replace(image);
// picture.findText("{{image}}").replace(image);
// picture.getText("{{image}}").replaceWithImage(image);
// picture.getText().findText("{{image}}").replace(image);
I believe your goal is as follows.
You want to replace {{Image}} on 1st page of Google Slides with the image.
The image can be retrieved by the URL like "https://chart.googleapis.com/chart?chs=300x300&cht=qr&chl="&M2.
In this case, how about directly using the values of column "M" as follows? When your script is modified, please modify it as follows.
From:
deck.insertImage("{{image}}", row[13]);
To:
deck.getSlides()[0].getShapes().find(s => s.getText().asString().trim().toUpperCase() == "{{IMAGE}}").replaceWithImage(`https://chart.googleapis.com/chart?chs=300x300&cht=qr&chl=${row[12]}`);
Note:
In this modification, it supposes that row[14] === "" is true and the column "M" has the value of https://chart.googleapis.com/chart?chs=300x300&cht=qr&chl=${value}. Please be careful about this.
It seems that there are a lot of rows you want to process. So, if the maximum execution time (6 minutes) is over, please separate the rows for processing. In your this question, you want to replace {{Image}} with the image. So, this answer is for it. Please be careful about this.
Reference:
replaceWithImage(imageUrl)

How to make a function to extract the new rich text links in Google Sheets while referencing specific ranges?

Some time ago, Google Sheets changed the adding of links to rich text, and now links cannot be found at the formula anymore. Personally I dislike this change very much because I use a custom function that extracts hyperlinks from cells with the old formula, and with the new change I just cannot find a way of doing this. I am not very good with functions yet, mainly the reason why I wrote my question title as detailed as possible.
What I need to do is to extract hyperlinks from cells using a custom formula, since it will need to be used among many other vanilla formulas. How can I set up a custom function/formula to extract the new hyperlinks based on range?
Here are the sheets where I want to extract links:
https://docs.google.com/spreadsheets/d/1JnSKQ7nd4J3NPRH4uSsOCYms-DF16j1pkCAuJeikToo/edit#gid=317867416
I would like to extract links from the games that are being posted, because I need to use those links elsewhere and I'd also like to have them ready to be imported if ever needed.
I need to specify a formula in another cell which will extract those links. For example =GETURL(B6) which would extract the new rich text hyperlinks based on a range that I insert for it.
Alternatively, is it possible to configure the document so that it makes links in the old format whenever inserted? This way I could try to workaround the new settings and future inserted links would go into the =HYPERLINK formula instead.
Many thanks!
I think this script would come in handy.
It gives the possibility to retrieve back the URL from a hyperlink formula.
Go to script editor, and create a new project.
Save the file.
Head up to Run > linkURL to run the script. This will create a new function in Sheets.
Let’s say cell A1 has the hyperlink in it. Go to any cell and type =linkURL(A1), and then hit Enter.
function linkURL(reference) {
var sheet = SpreadsheetApp.getActiveSheet();
var formula = SpreadsheetApp.getActiveRange().getFormula();
var args = formula.match(/=w+((.*))/i);
try {
var range = sheet.getRange(args[1]);
}
catch(e) {
throw new Error(args[1] + ' is not a valid range');
}
var formulas = range.getFormulas();
var output = [];
for (var i = 0; i < formulas.length; i++) {
var row = [];
for (var j = 0; j < formulas[0].length; j++) {
var url = formulas[i][j].match(/=hyperlink("([^"]+)"/i);
row.push(url ? url[1] : '');
}
output.push(row);
}
return output
}

Is there a function to convert a google doc to a google spreadsheet?

I need a way to transfer the data from a google document to be transferred over to a google spreadsheet (kind of backwards, I know). I need the line breaks in the document to be equivalent to starting a new cell right below. i.e. each line in the google doc has its own cell.
I've tried getting the body text from the google doc and setting the first cell as that variable but that only pastes the data in the single A1 cell (kind of want it to be similar to the way if you copy and paste doc body text into the A1 cell it will populate cells in the A column all the way down)
var body = openedFile.getBody().getText();
var newSpreadsheet = SpreadsheetApp.create('TEST').getId();
var testSpreadsheet = SpreadsheetApp.openById(newSpreadsheet);
testSpreadsheet.getRange('A1').setValue(bodyAll);
You want to put each paragraph of Document to the cells of Spreadsheet which was created in the script.
From your script, your Google Document has only texts. Your Google Document doesn't include tables, lists, images and so on.
If my understanding is correct, how about this modification?
Pattern 1:
In this pattern, body of var body = openedFile.getBody().getText() is splitted by \n. Then, the values are put to the created Spreadsheet.
Modifed script:
var openedFile = DocumentApp.openById("###"); // Please set Document ID here.
var body = openedFile.getBody().getText();
var bodyAll = body.split("\n").reduce(function(ar, e) {
if (e) ar.push([e]); // If you want to include the empty paragraph, please modify to ar.push([e]);
return ar;
}, []);
var newSpreadsheet = SpreadsheetApp.create('TEST');
newSpreadsheet.getSheets()[0].getRange(1, 1, bodyAll.length, bodyAll[0].length).setValues(bodyAll);
Pattern 2:
In this pattern, the paragraphs are retrieved from the body of Document, and each text is retrieved. Then, the values are put to the created Spreadsheet.
Modifed script:
var openedFile = DocumentApp.openById("###"); // Please set Document ID here.
var body = openedFile.getBody();
var bodyAll = body.getParagraphs().reduce(function(ar, e) {
var temp = e.getText();
if (temp) ar.push([temp]); // If you want to include the empty paragraph, please modify to ar.push([temp]);
return ar;
}, [])
var newSpreadsheet = SpreadsheetApp.create('TEST');
newSpreadsheet.getSheets()[0].getRange(1, 1, bodyAll.length, bodyAll[0].length).setValues(bodyAll);
Note:
In this script, the empty paragraphs are ignored. If you want to include the empty paragraphs, please modify the script like the comment.
References:
split()
getParagraphs()
reduce()
If above scripts didn't resolve your issue, I apologize. At that time, in order to correctly understanding your situation, can you provide a sample Document and a sample Spreadsheet of the result you want? Of course, please remove your personal information from them.

Apply a "row banding" theme to a range

I am a beginner to Google Apps Script but use it to automate some simple repeating tasks. I have several spreadsheets I am copying content on a weekly basis and export them as an .xls file that I send to my client.
I am trying to apply alternating colors to a range I copy from another sheet but I completely got stuck. How to correctly set bandingTheme with the applyRowBanding method? What is the right syntax I should use in the last line of my code?
My code:
function copyRange (SourceSSID, SourceRange, TargetSheetName, bandingTheme) {
var sheetSource = SpreadsheetApp.openById(SourceSSID);
var sheetTarget = SpreadsheetApp.openById("bla-bla");
var source = sheetSource.getRange(SourceRange);
var target_ss = sheetTarget.getSheetByName(TargetSheetName);
var values = source.getValues();
var target = target_ss.getRange(1, 1, values.length, values[0].length);
target.clear();
target.setValues(values);
target.applyRowBanding ();
}
If your method argument bandingTheme is one of the enums listed here, you can simply apply it, using the apply___Banding(BandingTheme theme) method signature:
target.applyRowBanding(bandingTheme);
The above is equivalent to this line, per documentation:
target.applyRowBanding(bandingTheme, true, false);
(In other words, the default behavior is to color the header but not the footer, in addition to alternating row colors.)
You can ensure no existing themes were previously present (only a single kind of alternating colors - be it from columns OR rows - can be present at any given time, else an error is thrown).
target.getBandings().forEach(function (banding) {
banding.remove();
});
/**
* set the new banding theme
* ....
*/
If you wanted to set a custom banding theme, you can do so by starting from one of the theme designs. Note that the apply___Banding methods return the Banding object that they applied. If you bind this return value (or chain the methods), then you can modify it using its class methods.
const newBanding = target.applyRowBanding(SpreadsheetApp.BandingTheme.BLUE);
// newBanding is now a Banding that was instantiated with the "Blue" template.
// Color the header column:
newBanding.setHeaderColumnColor('teal');
// Equivalent:
target.applyRowBanding(SpreadsheetApp.BandingTheme.BLUE).setHeaderColumnColor('teal');
Note that setting colors for non-header columns in a row-banding theme doesn't work. Likewise for setting non-header row colors in a column-banding theme.
If your bandingTheme argument isn't one of the theme enums, then you will have to provide more details about what it is in order to get answers that help you convert it into the available Spreadsheet Service methods.
Here is a simple function that removes existing bandings and then applies alternating colors to an entire sheet. Please refer to Google Apps Script Range and Banding Class documentation for support.
function applyRowBanding() {
let sheet = SpreadsheetApp.getActiveSheet();
let range = sheet.getRange(1, 1, sheet.getLastRow(), sheet.getLastColumn());
range.getBandings().forEach(banding => banding.remove());
range.applyRowBanding(SpreadsheetApp.BandingTheme.LIGHT_GREY, true, false);
}

Selecting text with google app script in Docs

Is it possible for an app script to highlight (as in select) text? I want to run the script from the menu and then have all matching instances of some text selected so they can be formatted in one go.
Specifically, I want to write a script to highlight all footnotes in a Google Doc so that they can be formatted simultaneously. I am the creator of the Footnote Stylist add on for Docs, which allows users to style footnotes. But I want to include the option of using any formatting, without having to include every available formatting choice in the add on itself.
How about skip the highlighting portion and just format them direct? The code below searches for the word "Testing" and bolds it & highlights it yellow. Hope this helps.
function bold() {
var body = DocumentApp.getActiveDocument().getBody();
var foundElement = body.findText("Testing");
while (foundElement != null) {
// Get the text object from the element
var foundText = foundElement.getElement().asText();
// Where in the element is the found text?
var start = foundElement.getStartOffset();
var end = foundElement.getEndOffsetInclusive();
// Set Bold
foundText.setBold(start, end, true);
// Change the background color to yellow
foundText.setBackgroundColor(start, end, "#FCFC00");
// Find the next match
foundElement = body.findText("Testing", foundElement);
}
}