Selecting text with google app script in Docs - google-apps-script

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);
}
}

Related

Converting formatted Text from Google sheets to Google docs

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

Find and format all words that start with a set string in Google Docs

I made a function in Google Apps Script that searches for all words in a Google Docs and changes their colors to a desired color. It takes as inputs: the doc ID, the color desired and the word to look for.
However, what I really need is a function that finds all the words that start with a particular string. For example, "change all words that start with # to blue". I tried messing with findText() but had no luck. Any ideas on how to fix the function below to do what I need? Thanks!
Currently, my function looks like this:
function colorTheWords(findMe,color,documentID) {
//color input must be formatted in CSS notation like '#ffffff'
//documentID must be formated as text in between ''
//findMe word must be formatted as ''
//open doc
var document = DocumentApp.openById(documentID);
var body = document.getBody();
var foundElement = body.findText(findMe);
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();
// Change the current color to the desired color
foundText.setForegroundColor(start, end, color);
// Find the next match
foundElement = body.findText(findMe, foundElement);
}
}
You can use regular expressions findText, which will allow you to do this easily. There is an answer to a similar question here:
Regex to check whether string starts with, ignoring case differences
I always use this site to help me to test my regular expressions before adding them to the code. Paste the contents of your document in and then fiddle with your regex until you just select what you need.
https://regexr.com/
The main issue you are encountering is that findText does not use normal regular expressions but a flavour called re2. This has some slight variations and restrictions. If you want to find all words that start with a specific string or character, this is the expression you should be using:
#([^\s]+)

Google App Script replaceText to replace only first occurrence of matched string

I would like to use google appscript to replace text on my google doc to convert it to PDF. But the problem is the function replaceText(textToReplace, newText); just remove every occurrence of the matched text. I just want to remove only the first occurrence. How to do that?
The replaceText method can be limited in scope to an element, by calling it on that element. But that does not help if the first paragraph where the text is found contains multiple instances of it: they are all going to be replaced.
Instead, use findText to find the first match, and then call deleteText and insertText to execute replacement.
// replaces the first occurrence of old
function replaceFirst(old, replacement) {
var body = DocumentApp.getActiveDocument().getBody();
var found = body.findText(old);
if (found) {
var start = found.getStartOffset();
var end = found.getEndOffsetInclusive();
var text = found.getElement().asText();
text.deleteText(start, end);
text.insertText(start, replacement);
}
}
If you think this ought to be easier, you are not alone.

How to copy ListItems from one Google Document to another while preserving numbering?

The accepted answer to How to copy content and formatting between Google Docs? indicates that we have to add conditional code just to copy elements. But I cannot get it to work for ListItem types, because the target document shows the list items without the original numbering.
var source_doc = DocumentApp.getActiveDocument();
var selection = source_doc.getSelection();
if (!selection) {
var ui = DocumentApp.getUi();
ui.alert('Please make a selection first.');
return;
}
var target_doc = DocumentApp.create('CopyOf'+DocumentApp.getActiveDocument().getName());
var target_body = target_doc.getBody();
var elements = selection.getRangeElements();
for (var i = 1; i < elements.length; i++) {
var source_element = elements[i].getElement();
var copy_element = source_element.copy();
if (copy_element.getType() == DocumentApp.ElementType.PARAGRAPH) {
target_body.appendParagraph(copy_element);
} else if (copy_element.getType() == DocumentApp.ElementType.LIST_ITEM) {
// This does not keep the numbering on the list item. Why?
target_body.appendListItem(copy_element);
// And playing games with setListId doesn't work either:
// copy_element.setListId(source_element);
// target_body.appendListItem(copy_element);
}
// TODO: Handle the other elements here.
}
The source document displays like this:
Target document renders like this:
How do I preserve ListItem formatting?
This seems much much harder than it should be: What I really want is to copy the users selection verbatim into a new document preserving all formatting, and from a google script.
It would seem that this could be done at a higher level. I can manually copy and paste and preserve the formatting, just not from the script.
I'm guessing that the cause of this is that there's a problem with using a Selection. Reading from a document directly seems to work fine.
Try appending the ListItem as text as a workaround.
target_body.appendListItem(copy_element.getText());
This will only copy the text though, not the formatting. You can also try to implement it by making a new list instead of copying the element directly. Here's a sample SO that might help.
I was having a similar problem (but not using a selection). It was being copied as a list but without any actual bullets. I just re-set the bullets manually like this:
target_body.appendListItem(copy_element).setGlyphType(DocumentApp.GlyphType.NUMBER)

How to add Named Ranges to sub-paragraph elements in Google apps-script

I would like to implement a 'html-span-like' feature inside Google Documents but whenever I try to add a NamedRange to a substring of text inside a Google Document, the range is merged with previous text in the same paragraph.
In result, the NamedRange is applied to the whole paragraph.
Here's my test case :
function createTextNamedRange(){
// Retrieve the current document's body
var doc = DocumentApp.getActiveDocument();
var docBody = doc.getBody();
// Add a new paragraph with text {NotNamed}
var para = docBody.appendParagraph('{NotNamed}');
// Append some text that will be named ( .appendText() method returns a Text element )
var textElem = para.appendText('{NamedText}');
// Build a range for the Text element
var range=doc.newRange().addElement(textElem).build();
// Name the range and append it to the document
doc.addNamedRange('myNamedRange',range);
}
I then display the content of the NamedRange using the Logger class in this function:
function getTextNamedRange(){
// Retrieve the named range
var namedRanges = DocumentApp.getActiveDocument().getNamedRanges();
// Iterate through each instance of name 'myNamedRange' (there's only one)
for (var nr in namedRanges){
var namedRange = namedRanges[nr];
// A range may contain several RangeElements, iterate through them
var rangeElements = namedRange.getRange().getRangeElements();
for(var re in rangeElements) {
// Get the text of each RangeElement and display it in the logger
var text = rangeElements[re].getElement().asText().getText();
Logger.log('Text with namedRange (' + namedRange.getName() + ') : "' + text +'"');
}
}
}
I would assume to get {NamedText} as an output but the Log Window tells this:
Text with namedRange (myNamedRange) : "{NotNamed}{NamedText}"
As it can be seen, the range was applied to the unmarked as well as the named text.
I found a workaround detailed below but it's definitely not to my taste: it consists of adding empty inline images between the .appendText() calls. This way the Text elements are not merged.
I'm still looking for a better solution.
The following is for WORKAROUND ONLY but appears to work.
It relies on InlineImage insertion before and after the Text chunk that has to be Named.
This prevent the NamedRange to merge when applied to chunk of Text inside a Paragraph
function createTextNamedRangeWorkaround(){
// WORKAROUND: prevent the Text Merging (and hence the NamedRange merging)
// by separating Text Elements with blank Inline Images
// Import a blank PNG file
// NOTE: This shouldn't be fetched each time
// but only once and passed as an argument
var blankImage = UrlFetchApp.fetch('http://upload.wikimedia.org/wikipedia/commons/d/d2/Blank.png');
// Retrieve the current document's body
var doc = DocumentApp.getActiveDocument();
var docBody = doc.getBody();
// Add a new paragraph with text {NotNamed}
var para = docBody.appendParagraph('{NotNamed}');
// WORKAROUND HERE :
para.appendInlineImage(blankImage.getBlob());
// Append some text
var textElem1 = para.appendText('{NamedText1}');
// WORKAROUND HERE :
para.appendInlineImage(blankImage.getBlob());
// Append some text
var textElem2 = para.appendText('{NamedText2}');
// Build Named ranges after text insertion (see notice below)
var range1=doc.newRange().addElement(textElem1).build();
// Name the range and append it to the document
doc.addNamedRange('Range1',range1);
var range2=doc.newRange().addElement(textElem2).build();
// Name the range and append it to the document
doc.addNamedRange('Range2',range2);
}
After execution the document is appended with a new paragraph containing three words separated by two blank inline PNGs
{NotNamed}{NamedText1}{NamedText2}
And the execution of my getTextNamedRange() shows the desired behaviour:
Text with namedRange (Range1) : "{NamedText1}"
Text with namedRange (Range2) : "{NamedText2}"
Important notices:
Named ranges must be applied alltogether after the last Text insertion, otherwise, previous NamedRanges adopt new insertions and appendings.
InlineImages can't be removed from parent Paragraph using image.removeFromParent() because this causes the Text elements (and the NamedRange) to be merged again