GAS is it possible to replace getActiveDocument().getSelection() at once? - google-apps-script

My User has the following selection in his Gdoc.
Now from the sidebar he wants to to replace the selection he made on the document. The GAS question is if it is possible to do that at once, something like:
var selection = DocumentApp.getActiveDocument().getSelection()
selection.replace("newtext")
Or do I have to loop through selection.getRangeElements() in order to delete them (or replace them) and than in someway place the new text in that position?

Not, that's not possible (well, if it is, it's not documented).
You have to loop through the selected elements, mainly because the selection may take part of paragraphs, forcing you to manage that. i.e. deleting just the selected part. And for completed selected elements, you can just remove them entirely (like images).
Here's an implementation on how to do this (part of the Kaylan's Translate script modified by me to properly replace images and partially selected paragraphs.
function replaceSelection(newText) {
var selection = DocumentApp.getActiveDocument().getSelection();
if (selection) {
var elements = selection.getRangeElements();
var replace = true;
for (var i = 0; i < elements.length; i++) {
if (elements[i].isPartial()) {
var element = elements[i].getElement().asText();
var startIndex = elements[i].getStartOffset();
var endIndex = elements[i].getEndOffsetInclusive();
var text = element.getText().substring(startIndex, endIndex + 1);
element.deleteText(startIndex, endIndex);
if( replace ) {
element.insertText(startIndex, newText);
replace = false;
}
} else {
var element = elements[i].getElement();
if( replace && element.editAsText ) {
element.clear().asText().setText(newText);
replace = false;
} else {
if( replace && i === elements.length -1 ) {
var parent = element.getParent();
parent[parent.insertText ? 'insertText' : 'insertParagraph'](parent.getChildIndex(element), newText);
replace = false; //not really necessary since it's the last one
}
element.removeFromParent();
}
}
}
} else
throw "Hey, select something so I can replace!";
}

Related

Google Apps Script. Get all links from document

Hi all) I need to get all links from google document. I found that general approach:
function getAllLinks(element) {
var links = [];
element = element || DocumentApp.getActiveDocument().getBody();
if (element.getType() === DocumentApp.ElementType.TEXT) {
var textObj = element.editAsText();
var text = element.getText();
Logger.log("text " + text);
var inUrl = false;
for (var ch=0; ch < text.length; ch++) {
var url = textObj.getLinkUrl(ch);
if (url != null) {
if (!inUrl) {
// We are now!
inUrl = true;
var curUrl = {};
curUrl.element = element;
curUrl.url = String( url ); // grab a copy
curUrl.startOffset = ch;
}
else {
curUrl.endOffsetInclusive = ch;
}
}
else {
if (inUrl) {
// Not any more, we're not.
inUrl = false;
links.push(curUrl); // add to links
curUrl = {};
}
}
}
}
else {
var numChildren = element.getNumChildren();
for (var i=0; i<numChildren; i++) {
links = links.concat(getAllLinks(element.getChild(i)));
}
}
Logger.log(links);
}
It works perfectly fine if i, for example, type url in text, but if add link via menu ("Insert" -> "Link") it doesn't work, function getLinkUrl() returns null. Documentation contains info about Link class, i thought all links represented by it, but don't understand why i can't get link inserted via menu.
I thought maybe i can use some regular expression on text of document element, but if i add link via menu item i can specify custom label for link, which may not contain url in it.
Have anyone faced this scenario? What i missed?

Google Apps Script; Docs; convert selected element to HTML

I am just starting with Google Apps Script and following the Add-on quickstart
https://developers.google.com/apps-script/quickstart/docs
In the quickstart you can create a simple add-on to get a selection from a document and translate it with the LanguageApp service. The example gets the underlying text using this:
function getSelectedText() {
var selection = DocumentApp.getActiveDocument().getSelection();
if (selection) {
var text = [];
var elements = selection.getSelectedElements();
for (var i = 0; i < elements.length; i++) {
if (elements[i].isPartial()) {
var element = elements[i].getElement().asText();
var startIndex = elements[i].getStartOffset();
var endIndex = elements[i].getEndOffsetInclusive();
text.push(element.getText().substring(startIndex, endIndex + 1));
} else {
var element = elements[i].getElement();
// Only translate elements that can be edited as text; skip images and
// other non-text elements.
if (element.editAsText) {
var elementText = element.asText().getText();
// This check is necessary to exclude images, which return a blank
// text element.
if (elementText != '') {
text.push(elementText);
}
}
}
}
if (text.length == 0) {
throw 'Please select some text.';
}
return text;
} else {
throw 'Please select some text.';
}
}
It gets the text only: element.getText(), without any formatting.
I know the underlying object is not html, but is there a way to get the selection converted into a HTML string? For example, if the selection has a mix of formatting, like bold:
this is a sample with bold text
Then is there any method, extension, library, etc, -- like element.getHTML() -- that could return this?
this is a sample with <b>bold</b> text
instead of this?
this is a sample with bold text
There is a script GoogleDoc2HTML by Omar AL Zabir. Its purpose is to convert the entire document into HTML. Since you only want to convert rich text within the selected element, the function relevant to your task is processText from the script, shown below.
The method getTextAttributeIndices gives the starting offsets for each change of text attribute, like from normal to bold or back. If there is only one change, that's the attribute for the entire element (typically paragraph), and this is dealt with in the first part of if-statement.
The second part deals with the general case, looping over the indices and inserting HTML markup corresponding to the attributes.
The script isn't maintained, so consider it as a starting point for your own code, rather than a ready-to-use library. There are some unmerged PRs that improve the conversion process, in particular for inline links.
function processText(item, output) {
var text = item.getText();
var indices = item.getTextAttributeIndices();
if (indices.length <= 1) {
// Assuming that a whole para fully italic is a quote
if(item.isBold()) {
output.push('<b>' + text + '</b>');
}
else if(item.isItalic()) {
output.push('<blockquote>' + text + '</blockquote>');
}
else if (text.trim().indexOf('http://') == 0) {
output.push('' + text + '');
}
else {
output.push(text);
}
}
else {
for (var i=0; i < indices.length; i ++) {
var partAtts = item.getAttributes(indices[i]);
var startPos = indices[i];
var endPos = i+1 < indices.length ? indices[i+1]: text.length;
var partText = text.substring(startPos, endPos);
Logger.log(partText);
if (partAtts.ITALIC) {
output.push('<i>');
}
if (partAtts.BOLD) {
output.push('<b>');
}
if (partAtts.UNDERLINE) {
output.push('<u>');
}
// If someone has written [xxx] and made this whole text some special font, like superscript
// then treat it as a reference and make it superscript.
// Unfortunately in Google Docs, there's no way to detect superscript
if (partText.indexOf('[')==0 && partText[partText.length-1] == ']') {
output.push('<sup>' + partText + '</sup>');
}
else if (partText.trim().indexOf('http://') == 0) {
output.push('' + partText + '');
}
else {
output.push(partText);
}
if (partAtts.ITALIC) {
output.push('</i>');
}
if (partAtts.BOLD) {
output.push('</b>');
}
if (partAtts.UNDERLINE) {
output.push('</u>');
}
}
}
}
Ended up making a script to support my use-case of bold+links+italics:
function getHtmlOfElement(element) {
var text = element.editAsText();
var string = text.getText();
var indices = text.getTextAttributeIndices();
var output = [];
for (var i = 0; i < indices.length; i++) {
var offset = indices[i];
var startPos = offset;
var endPos = i+1 < indices.length ? indices[i+1]: string.length;
var partText = string.substring(startPos, endPos);
var isBold = text.isBold(offset);
var isItalic = text.isItalic(offset);
var linkUrl = text.getLinkUrl(offset);
if (isBold) {
output.push('<b>');
}
if (isItalic) {
output.push('<i>');
}
if (linkUrl) {
output.push('<a href="' + linkUrl + '">');
}
output.push(partText);
if (isBold) {
output.push('</b>');
}
if (isItalic) {
output.push('</i>');
}
if (linkUrl) {
output.push('</a>');
}
}
return output.join("");
}
You can simply call it using something like:
getHtmlOfElement(myTableCell); // returns something like "<b>Bold</b> test."
This is obviously a workaround, but you can copy/paste a Google Doc into a draft in Gmail and then that draft can be turned into HTML using
GmailApp.getDraft(draftId).getMessage().getBody().toString();
I found this thread trying to skip that step by going straight from a Doc to HTML, but I thought I'd share.

Replace selection using Google Suite API

I'm trying to replace a selection with an image using Google Suite API.
I figured that if I can get the index of the selection I can insert an image at that specific index and then remove every selected element. However, I can't seem to get the index working correctly.
I've tried the following, but it only returns -1, even though I'm selecting a word in the middle of the text.
var doc = DocumentApp.getActiveDocument();
var selection = doc.getSelection();
var elements = selection.getRangeElements();
if(elements.length > 0){
return elements[0].getStartOffset();
}
I've tried searching the documentation, but can't find a solution.
What I want is something like:
var selection = doc.getSelection();
selection.replaceWithImage(image);
Thanks in advance.
I have a script which helps me locate things inside of a document. In fact, I used it last night to create an envelope printer script for Google Docs.
Here's a link to it. I'm always changing it to fit my particular needs so feel free to have fun with. It will help you to find images too.
I solved it by moving the position of the cursor to the beginning of the selection. Here is the code commented for others having the same question.
function insertImage(imageURL){
var doc = DocumentApp.getActiveDocument();
var body = doc.getBody();
var UI = DocumentApp.getUi();
var blob = UrlFetchApp.fetch(imageURL).getBlob();
var cursor = doc.getCursor();
/* If cursor is null, there is a selection not a position */
if(!cursor){
var selection = doc.getSelection();
/* Get all elements selected */
var elements = selection.getRangeElements();
if(elements[0]){
var element = elements[0].getElement();
var index;
var previousElement = element.getPreviousSibling();
var fakedElement = false;
var position;
/* If the element is type TEXT you can't get index from getChildIndex. */
if(element.getType().toString() == "TEXT"){
index = elements[0].getStartOffset();
if(element.getText() === " "){
element = doc.insertParagraph(index, "");
fakedElement = true;
}
} else {
index = element.getParent().getChildIndex(element);
}
/* If the selected element is an image, we need to append a fake paragraph to keep the cursor poisition. Dont worry, this will be removed later. */
if(elements.length === 1 && element.getType().toString() == "INLINE_IMAGE"){
element = doc.insertParagraph(index, "");
fakedElement = true;
}
/* Go through each element and remove it. */
elements.forEach( function (element, key) {
var text;
if(element.getElement().editAsText){
if(element.isPartial()){
text = element.getElement().editAsText();
text.deleteText(element.getStartOffset(), element.getEndOffsetInclusive());
} else {
doc.appendParagraph(''); // Create empty paragraph since you can't delete last paragraph.
text = element.getElement().removeFromParent();
}
} else {
element.getElement().removeFromParent();
}
});
if(body.getNumChildren() === 1){
paragraph = doc.getBody().appendParagraph('');
position = doc.newPosition(paragraph, 0);
doc.setCursor(position);
} else if(element && !element.getParent() && previousElement){
element = previousElement;
index = element.getParent().getChildIndex(element);
} else if(body.editAsText().getText().length === 0){
paragraph = doc.getBody().appendParagraph('');
position = doc.newPosition(paragraph, 0);
doc.setCursor(position);
} else {
position = doc.newPosition(element, index);
}
if(!position){
paragraph = doc.getBody().appendParagraph('');
position = doc.newPosition(paragraph, 0);
doc.setCursor(position);
}
/* Move position of cursor to the new position */
doc.setCursor(position);
/* Update cursor variable since its now available */
cursor = doc.getCursor();
/* If a placeholder element was created to keep the position, remove it. */
if(fakedElement){
element.removeFromParent();
}
}
}
/* Insert image */
var image = body.appendImage(blob);
cursor.insertInlineImage(image);
image.removeFromParent();
return true;
}

How do I find and select a next bold word

From the https://gist.github.com/oshliaer/d468759b3587cfb424348fa722765187 , It is possible to select a particular word from the findText, I want to implement the same for bold words only
I have a function to find bold. How do I modify the above gist?
var startFlag = x;
var flag = false;
for (var i = x; i < y; i++) {
if (text.isBold(i) && !flag) {
startFlag = i;
flag = true;
} else if (!text.isBold(i) && flag) {
flag = false;
rangeBuilder.addElement(text, startFlag, i - 1);
doc.setSelection(rangeBuilder.build());
return;
}
}
if (flag) {
rangeBuilder.addElement(text, startFlag, i - 1);
doc.setSelection(rangeBuilder.build());
return;
}
Let's assume another algorithm
/*
* #param {(DocumentApp.ElementType.LIST_ITEM | DocumentApp.ElementType.PARAGRAPH)} element
*/
function hasBold(element, start) {
var text = element.editAsText();
var length = element.asText().getText().length;
var first = -1;
var end = -1;
while (start < length) {
if (first < 0 && text.isBold(start)) {
first = start;
}
if (first > -1 && !text.isBold(start)) {
end = start - 1;
return {
s: first,
e: end
}
}
start++;
}
if (first > -1) {
return {
s: first,
e: length - 1
}
}
return false;
}
It's not clean but I've tested it and it works fine.
hasBold lets us finding bolds in the current element.
Finally, we have to loop this feature within document.getBody().
You could to get the full code here find next bold text in google document.
Also you could try it on a copy
A new idea
The Direct searcing
The best way is to use a callback while it is checked
var assay = function (re) {
var text = re.getElement()
.asText();
for (var offset = re.getStartOffset(); offset <= re.getEndOffsetInclusive(); offset++) {
if (!text.isBold(offset)) return false;
}
return true;
}
function findNextBold() {
var sp = 'E.';
Docer.setDocument(DocumentApp.getActiveDocument());
var rangeElement = Docer.findText(sp, Docer.getActiveRangeElement(), assay);
rangeElement ? Docer.selectRangeElement(rangeElement) : Docer.setCursorBegin();
}
The Approx searching
var assay = function(re) {
var text = re.getElement().asText();
var startOffset = re.getStartOffset();
var endOffset = re.getEndOffsetInclusive() + 1;
for (var offset = startOffset; offset < endOffset; offset++) {
if (!text.isBold(offset)) return false;
}
return this.test(text.getText().slice(startOffset, endOffset));
}
function findNextBold() {
var searchPattern = '[^ ]+#[^ ]+';
var testPattern = new RegExp('^[A-Z0-9._%+-]+#[A-Z0-9.-]+\.[A-Z]{2,4}$');
Docer.setDocument(DocumentApp.getActiveDocument());
var rangeElement = Docer.findText(searchPattern, Docer.getActiveRangeElement(), assay.bind(testPattern));
rangeElement ? Docer.selectRangeElement(rangeElement) : Docer.setCursorBegin();
}
Docer
Yes. it is possible to find bold text. You need to use findText(searchPattern) to search the contents of the element for the specific text pattern using regular expressions. The provided regular expression pattern is independently matched against each text block contained in the current element. Then, use isBold() to retrieve the bold setting. It is a Boolean which returns whether the text is bold or null.

How to insert a paragraph object in listItem preserving the formating of each word of the paragraph?

Following the documentation sample, I'm trying to create a function that search for a numerated list in a google document and, if it finds it, adds a new item to the list. My code works well (thanks to #Serge insas for previous help) with strings, but not with paragraphs objects. I know I could get the paragraph text and add it to listItem, but then I lose the formating. Is there a way to insert a paragraph preserving all it's formating? (I know I could use var newElement = child.getParent().insertListItem(childIndex, elementContent.getText()) do insert text without words formating)
Here the code:
function test() {
var targetDocId = "1A02VhxOWLUIdl8LTV1tt2S1yASDbOq77VbsUpxPa6vk";
var targetDoc = DocumentApp.openById(targetDocId);
var body = targetDoc.getBody();
var elementContent = targetDoc.getChild(2); // a paragraph with its formating
var childIndex = 0;
for (var p= 0; p< targetDoc.getNumChildren(); p++) {
var child = targetDoc.getChild(p);
if (child.getType() == DocumentApp.ElementType.LIST_ITEM){
while(child.getType() == DocumentApp.ElementType.LIST_ITEM){
child = targetDoc.getChild(p)
Logger.log("child = " + child.getText())
childIndex = body.getChildIndex(child);
Logger.log(childIndex)
p++
}
child = targetDoc.getChild(p-2);
var listId = child.getListId();
if (child.getText() == '') {
childIndex = childIndex -1;
}
Logger.log(childIndex)
var newElement = child.getParent().insertListItem(childIndex, elementContent);
newElement.setListId(child);
var lastEmptyItem = targetDoc.getChild(childIndex +1).removeFromParent();
break;
}
Here a screen shot of my targetDoc (note the second item, Paragraph):
I know this question is old, but I've come up with a solution and will leave here for anyone that may need it. It is not complete, as I have yet to find a way to copy any Inline Drawing and Equation to a new element...
Anyways, here is my code, it will work well if the paragraph you want to convert to a list item only has text and Inline Images.
function parToList() {
var doc = DocumentApp.getActiveDocument();
var body = doc.getBody();
//gets the paragraph at index 1 on body -> can be changed to what you want
var par = body.getChild(1);
var childs = [];
for (var i = 0; i<par.getNumChildren(); i++) {
var child = par.getChild(0);
childs.push(child);
child.removeFromParent();
};
par.removeFromParent();
//puts the list item on index 1 of body -> can be changed to the wanted position
var li = body.insertListItem(1, "");
childs.reverse();
for (var j in childs) {
var liChild = childs[j];
var childType = liChild.getType();
if (childType == DocumentApp.ElementType.EQUATION) {
//still need to find a way to append an equation
} else if (childType == DocumentApp.ElementType.INLINE_DRAWING) {
//still need to find a way to append an inlineDrawing
} else if (childType == DocumentApp.ElementType.INLINE_IMAGE) {
li.appendInlineImage(liChild);
} else if (childType == DocumentApp.ElementType.TEXT) {
li.appendText(liChild);
};
};
};
Cheers